Monday, October 21, 2019

Procedural City Update

It's been a few months since I posted something to the 3DWorld blog. I've been busy with various other things and most of the work I've done on 3DWorld has been optimizations, bug fixes, and cleanup. I've made various minor improvements to my procedural city in the past few months:
  • Grass is now properly scaled to match buildings (it was previously much too large)
  • Added chimneys to some of the generated houses
  • Secondary buildings now show up in overhead map view
  • Overhead map view optimizations (~2x faster)
  • Added doors to some building types (cube based buildings for now) 
  • Pedestrian destinations now include parked cars in parking lots (in addition to buildings)
  • Fixes/improvements to tunnel end point placement to reduce the oversized facades
  • Bug fixes to building and tunnel ray casting/queries and collision detection
I'll provide more detailed descriptions of some of these items and a few screenshots showing what I've done.

Grass Scale

the original grass scale was way off. Grass blades were half the height of the door in the house below. This was because I was using the same grass scale/size for both ground and tiled terrain mode, even though distance scales are different in these modes. The grass looked bad enough that I disabled it when capturing most of my city screenshots in previous blog posts. It was time to fix that.

I've finally scaled the grass 4x smaller, and it looks much better. I suppose it's still a bit too large. I think it's good enough though. Smaller grass comes at a somewhat reduced framerate because there are more blades drawn. Technically, the level of detail (LOD) falloff scales as well, so there should be the same number of blades. In reality the LOD is determined based on the distance between the camera/player and the closest point of the grass block within a tile. This means that LOD can only be applied per grass block and the nearest blocks must all be the highest detail. I lose some fine LOD control due to the coarse granularity of the LOD blocks. If I make the grass much smaller (denser), the performance will be unacceptable.

Here's a screenshot showing the current quarter length grass height in tiled terrain mode.

Houses with more realistically sized grass blades. They're still a bit large, but much better than they were before.

House Chimneys

I decided to add chimneys to 67% of houses. This makes them look a bit more interesting and realistic. Now houses can have multiple sections, sloped roofs, doors, porches, garages, sheds, and chimneys. I wonder what I'll add next? Swimming pools? A screenshot of some houses with chimneys is shown below.

Procedurally generated houses now have a 67% chance of having a chimney.

Overhead Map View

I went back and made another pass at city and building support in overhead map mode and made two significant changes. First of all, secondary buildings are now shown. There are a lot of them, as can be seen in the image below.

Overhead map mode showing both city buildings and secondary buildings (and all cars!) at around 12 FPS.

Map view can be zoomed out to show the entire city with pixel-sized buildings, and zoomed in to show individual (moving) cars and benches filling the screen.

Overhead map view zoomed in on a city showing buildings, individual parked and moving cars, and benches.

This looks good, but really hurt performance. Map draw time was increased from 100ms (10 FPS) to around 160ms (6 FPS). [Note that this mode colors each pixel on the CPU by doing ray queries into the scene, which is flexible but somewhat slow compared to drawing on the GPU.] The framerate doesn't have to be that high for map view because the player isn't walking around. It only supports pan and zoom with the arrow keys and mouse, similar to Google Maps. However, 160ms made panning feel too slow. I made various optimizations such as adding improved acceleration structures for querying buildings and cars. Now draw time is down to around 80ms (12.5 FPS), which is about half of what it previously was.

Building Doors

I added building doors to complement my house doors. These use a different texture of a pair of glass doors rather than the single white wooden door used for houses. Each building has between one and four pairs of doors randomly assigned to an exterior wall on one of the four sides. Doors look reasonably good on brick and stone buildings, but tend to conflict with windows and other features present in the textures of metal/glass office buildings. The problem is that I can't move the windows around to avoid the door in these buildings because I'm not placing individual windows. They're part of the building textures. Here are some screenshots of what these doors look like on various building types and textures.

Door placed on the side of a concrete block building. Note that the entire bottom row of windows has been removed so that the door doesn't overlap them.

Door placed in a glass office building. The style of the door matches that of the building, though the vertical height isn't quite aligned with the windows.

Door placed in another office building. The height is correct compared to the windows, but it's placed over/next to a window.

The door texture I used contains a reflection that doesn't match the scene in 3DWorld. Almost all of the real door textures (from photographs) I was able to find online had reflections. I tried filling the glass windows in the doors with a uniform gray, but that made them look too artificial. So for now I'll keep the false reflections.

Maybe at some point I can generate real reflections for nearby doors as the player moves around the scene using a system similar to how 3DWorld does water plane reflections. Or maybe I could use the system I have for cube map reflections in ground mode. However, I'm not sure how many people would actually notice the reflection on such a small door. It's a lot of code complexity, development effort, and frame time dedicated to a minor feature that could go unnoticed.

Pedestrian Parked Car Destinations

The original pedestrian navigation system chose random buildings as destinations. Once a pedestrian reached the building, he/she was re-spawned at a random location outside the player's view. I decided that it would be interesting to have some of the pedestrians (25%) choose to walk to parked cars instead to add a bit of variety. This mostly worked, but I did encounter two unexpected challenges.

First, some of the parked cars were walled in between other parked cars such that pedestrians couldn't reach them without intersecting the bounding cubes of surrounding cars. There isn't always enough space between parked cars for pedestrians to fit. They would just walk around the block of parked cars and never reach their destinations. The fix was to group blocks of contiguous cars in a parking lot into a bounding cube and consider the pedestrian at their destination on a collision with any car in the group, even if it wasn't the correct car. How can the player tell which car was the destination anyway? Yes, I do have debug visualizations for this, but that's really for my own use. Problem solved.

The second problem was related to using separate threads for updating cars and pedestrians. This caused 3DWorld to crash after running the simulation for a few minutes. I was eventually able to track it down to the code that sorted cars by city and distance to the player. This sorted parked cars as well as moving cars. If a pedestrian happened to query a car that was in the process of being moved by the sort algorithm on the other thread, it could occasionally read incorrect values that lead to asserts and other odd behavior. I tried a number of fixes that failed to solve the problem, then came up with something simple. All that's really needed is the bounding cube of the car for collision detection purposes. I can simply generate a vector of bounding cubes for the cars in each parking lot, and use that for pedestrian destinations and collision detection. Since this vector never has to be sorted, the other thread won't touch it and everything works correctly. That changed fixed the second problem, and now car destinations are working.