Fugue Devlog 8: Testing Outdoor Environments and Performance

· 04.23.2021 · projects/fugue

I started experimenting with an outdoor environment to start trying out the various environmental effects and elements I looked into last week.

There were a few small victories: the Waterways add-on worked great for still pools by setting both Flow > Distance and Flow > Pressure to 0. I also found an off-the-shelf cloud shader that looks great. With these elements and the ones from last week the outdoor environment looked dreamy:

But the experience raised an important issue that I kind of hoped I would avoid: performance. The outdoor scene was very choppy. I originally had the ~6.6k vertices trees so I tried simplifying them down to about ~1.2k or so. It didn't seem to help much. After reading up on 3d game optimization, the vertex/triangle count was not as important as I thought.

It's possible that some slowdown is due to Godot, which isn't as optimized as more mature 3d game engines (for example, it lacks occlusion culling, which is when objects blocked/hidden by other objects are discarded). It sounds like major 3d improvements have been pushed to the 4.0 release, which may not happen for awhile.

My GPU (or rather my lack of a dedicated one) is no doubt a major bottleneck here, but I want this game to play well on integrated graphics. I'm going with the low-res textures and low-poly models not only for style reasons but also for performance. I thought that would go a long way in terms of performance, but I was completely wrong. They are important, but not nearly as important as light and shadows.

After some more reading and testing of my own:

  • Shadows have a huge effect. Turning off ambient occlusion for the world environment helps a lot and using simpler shadows helps a bit more. Glow doesn't seem to affect FPS that much.
  • Mesh amount and complexity still have an impact. Having a lot (~100) of the 1.2k vertices trees did slow things down a lot, if only because they make light/shadows more complicated.
  • The wind shader, on the other hand, didn't seem to affect performance much compared to a regular spatial shader. Though it does require dynamic shadows to work; otherwise I could probably bake those shadows.
  • The cloud shader also has a pretty substantial impact (10-15fps). Tweaking its settings to do fewer draw passes did help a little. I also tried porting another cloud shader, but that performed even worse and was a hassle to set up.

Throughout this testing I thought this was just an issue with the outdoor scene (with the high number of moving trees, especially), but the indoor test scene also had an FPS of only about 10-11. Turning off ambient occlusion, however, boosted that to around 30fps. I guess I completely underestimated how demanding ambient occlusion is.

Specifically, I'm using Godot's screen-space ambient occlusion (SSAO), which adds a lot of richness to the environment. But it's hard to justify such a massive drop in FPS. Fortunately, the screen shader effect kind of maintains some visual interestingness on its own.

I still wanted to preserve some AO if possible. Baking lightmaps is the usual approach and wasn't too complicated to get set up in Godot. The baked lighting does looks better than having no SSAO, and I get around 30fps consistently—basically same performance as without the baked lightmap. But the baked lightmaps don't look as good as the SSAO. According to the docs SSAO is a different approach that captures details in smaller geometries. But after digging a bit, it seems the main difference is that SSAO has a "Light Affect" parameter that controls the visibility of AO in direct light. Normally it's not visible in direct light, i.e. "Light Affect" is 0, but I set it to 1 to exaggerate corners with deeper shadows. This isn't an option for the AO produced by baking lightmaps.

So I tried another route. The screen shader effect I'm using already simulates half resolution for more pixel texture; it makes sense to just actually render at half resolution and draw fewer pixels in the first place. Below are a few comparisons:

1x resolution, with a screen shader that simulates 0.5x res. Runs at about 20-22 FPS

0.5x resolution, with a screen shader that simulates (an additional) 0.5x res. Runs at about 48-54 FPS

0.5x resolution, without a screen shader further downscaling. Runs at about 54-60 FPS

1x resolution, with a screen shader that simulates 0.5x res and SSAO enabled. Runs at about 15-18 FPS

0.5x resolution, with a screen shader that simulates (an additional) 0.5x res and SSAO enabled. Runs at about 50-60 FPS

0.5x resolution, without a screen shader further downscaling and SSAO enabled. Runs at about 40-44 FPS

The outdoor scene looks good under all settings. But I imagine the double downscaling looks worse as there are more objects on the screen. That's the case with the indoor scene, which looks too noisy with the double downscaling. But otherwise the FPS gains are very promising and give me enough space to switch SSAO back on for the indoor scene!

One issue here is that this downscales the entire viewport, so the text is also downscaled. Text should probably render at the highest resolution possible for readability. So instead I need to render the player camera to a separate downscaled viewport and then render the non-downscaled resolution text (and other UI elements) on top. This was surprisingly complicated to set up; at least, the way Godot's viewports and scene tree work isn't intuitive to me. I struggled to find a way to specify what node loaded scenes attach to so that I could automatically attach scenes to my downscaled viewport node. But that doesn't seem possible—instead I have my player camera as a child of this downscaled viewport node and let scenes attach to the scene tree normally. The "Main" scene that contains all of this is instantiated as a singleton (i.e. using autoload).

This is the route I'll go with for now. The look of the game is pretty much the same, and I can use SSAO for indoor scenes and maintain high framerates. Similarly, the outdoor scene can use the cloud shader and maintain high framerates. That will likely change though as one tree is obviously an unrealistically simple scene. I need to keep experimenting with simpler trees and laying out the scene differently so I don't actually have to have a small forest's worth of trees. My hope is that with this set up the marginal cost of additional meshes will be pretty small. Not small enough for an entire forest, but small enough that I won't have to worry much about performance again.

A related question is the player camera. Right now the player has free reign over it, and the dialogue system was designed with that in mind. However when I imagine the game being played I see a fixed camera setup. The benefit with that is the player is way more constrained in what they see, so I can be more minimal in scene dressing and maintain better performance that way. For example, I'd only need to seat a few trees at the edge of the camera's view rather than anywhere the player might possibly look.

Fixed camera example

This was just a quick test to see how it might work. To really see if this is the right approach I should start figuring out the character design more and flesh out the third-person system in general. So I'll probably tackle that next before prototyping more of the outdoor/natural environments.