
ProcJam 25 Diary - p2 a 3D dungeon

This is the second part of my 2025 procjam diary. The previous entry is here.
The floor plan above doesn't look much different from where we left off yesterday. Apologies for all the lines and garish saturated colours (Its a screenshot from the game, where I actually float the map in the 3d scene space alongside the geometry to compare, hence why its a bit grungy). Anyway the main difference with this stage of the process is that I have done another pass across the grid to properly mark walls for rooms (cyan) and walls for corridors (blue)
I've also flagged some tiles on the room walls (blue again) to indicate extra holes I want to punch through the geometry. This is just because Id like to give the buildings a sort of decayed look.
Tiles to Blocks
The actual 3d construction of the world is done in a very "minecraft" kind of way. For most of the tiles in the map I simply place a 3d cube with the appropriate texture.

The image above shows the corridor area to the bottom right of the map (the one at the top of this post). I've removed the ceiling tiles from most of it to show how the interior looks. Because we are dealing with an FPS game corridors are 1 cube wide but their containing walls are 2 cubes high. Each map layer also has a sort of Above and Below set of cubes that is implicitly defined by the room or corridor and its total height. The corridor section above looks like this in First Person (obviously with the ceiling tiles back on )

And here is an example of a simple room generated in the same way

I've masked out the rest of the scene in photoshop so its easier to see the details of the specific room we are looking at. You might be able to see that this room is on an upper floor (check the previous post to see how I'm generating the different floor maps). The room has various exits, some chunks missing from the walls and even a ladder leading down to a lower level (by the red light on the floor). The ladder takes the place of the "spiral staircase" tiles in the 2d map generator pass. The light on the wall is part of a prop decoration phase that I will talk about later.
Hopefully you can see that the cubes aren't perfect cubes, in fact they are all a bit wonky. why?
Sorry Plato
People can sense pure geometric forms. Using them in games can make the environments feel synthetic and artificial. That's fine in loads of contexts, but not if you want to represent a decaying or organic world. So instead of using perfect cubes my system generates meshes for its building blocks and then subtly wobbles the edge/corner points to add imperfections and a create a more organic feel

This process is a little more complex than it might seem at first because you cant just add a wobble to every edge. If you do, there is a strong chance you would introduce weird discontinuities where a stack of blocks has gaps appear half way up it. A block on the top of a wall can have a wobbly upper surface, but not a wobbly lower edge. My code accounts for this by creating a bitmasking neighbourhood value for every cube in the 3d array. This is similar to how 2D tile-map sprite games often represent terrain types and calculate how walls should be drawn. There's a good explanation here.
The cubes use a single counter to track the empty spaces around them in each of the 3d axes. For example a space to the north counts +1, south +2, east +4, west +8, up +16, down +32. The final number can be checked to see exactly where the cube should have solid faces, and where they connect to neighbouring cubes.. You end up with 64 possible meshes. My code builds all of these and then wobbles the edges and corners that it knows are ok to wobble (i.e. they don't form seams with neighbouring blocks).

This process also gives us free back-face culling (as seen above), since we don't need to generate the faces that will not be seen.
My system actually makes several copies of the entire 64 cube set, each with different random wobbles. This might seem arduous, but it can all be done in a fairly simple code loop, and the resulting meshes are pulled into the 3d world wherever a block is needed. The final result behaves like a sort of wonky lego set. The 2d layered building map is therefore turned into a 3d array of cubes with wonky edges!

AutoPainting
Now as we have seen I will do everything I can in code to avoid having to do any modelling or scene arrangement. The same is true for texturing. The meshes generated in the phase explained above have no UVs (texture coordinates) and no associated materials. They are simply meshes with verts and normals.
One advantage of using cubes (or near cubes) for the construction process is that I can apply textures to them based on their positions and angles in the world (which I know will always be rectinlinear). I do this using a series of tri-planar shaders I wrote specifically for the project.

You will have seen tri-planar shaders before, there is an excellent tutorial on building them from catlikecoding (also the source of the image above). They basically use the normals of a mesh combined with its world position to apply a texture to it.
Its as if the texture is projected from a certain side of the world and will splat itself onto any surface that faces the projector. Since my cubes are always facing consistent directions I can rely on a triplanar projection to texture them in a seamless tiling way. As long as the repeating interval of the texture in my shader matches the world scale of a cube the mapping should always line up.
Now of course I wasn't happy with just a basic shader, so I made something that adds some variance to the texturing. The following screenshot shows the material parameters I am using.

Firstly we have 2 main textures, one that is applied to surfaces facing NSEW and another for surfaces facing UP or DOWN. These two textures also have metallic and normal counterparts (just like a normal PBR style shader). But I also have 2 other textures involved. A gradient map and a noise map.
The noise map is read in a similar way to the 2 axis maps I just mentioned, and creates a pseudo 3d noise sampler. The values from this noise map are then used to cross-mix the other textures with a sort of masking. I use this mostly on the floors to generate a more organic mix of materials. (Seen below in the mix of the diamond grill texture and the rebar-concrete texture)

The gradient map has a slightly different use. It applies a colour tint to the blocks, based on the world position of the block and a scaling value. Its similar to how sedimentary rock has different layers of colours stacked vertically in its structure.
This gradient lookup is also modified by the 3d noise which disrupts any obvious stripes and results in a subtle sediment style mottling effect on the walls.

I'm pretty happy with the overall results which create a more realistic and organic sense of decaying architecture. The variations in mesh edges combined with the noise influenced shaders means that the repetition commonly seen in PCG structures is softened a bit.
In the next article I'm going to cover probably the last part of the construction process, adding props and anomalies to the scene.





