I decided to make this article to help anyone that wants to start walking on the “Open World on Unreal Engine 4” path.
The engine version used at the time of this article was 4.16, so some stuff could be deprecated or could have new tech support 🙂
This has been a great adventure, with a lot of issues, fun and pain. It would be great if I could help avoid some pain to someone else, even in the little things.
Hope you enjoy it 🙂
Die Young production started with an interesting idea from an artistic point of view. It was a crazy challenge for us: we had to develop an open world set in a mediterranean island, using UE4 (4.01 wasn’t ready for open world development), and we were just 6, and only 4 had previous experiences in game development.
The first six months were pretty though, the engine got a lot of updates in that period but still wasn’t enough powerful to run our game.
We decided to spend the first year studying the engine and working on a prototype, which we presented at Gamescon in 2015.
What’s the problem dude? It’s Unreal Engine 4!
Yeah, and it’s fantastic! But it’s an undeniable fact that it was lacking a lot of rendering optimization on the first versions, not a little problem for an open world production.
Many rendering optimizations updates came during 2015, together with them Epic Games released Kite Demo, which ran 30 FPS on a GTX 970: that performance was half of our goal (60fps on a GTX 970).
We continued the development of the prototype: the only reason why we got it running at 50-60 fps on a GTX 970 was that Tommaso Magherini, our Level Designer, smartly built a level made of “corridors” which helped occluding almost everything of the rest of the map, but the production didn’t want this: the goal was a game environment made of long sights and fields full of vegetation.
After months of prototyping, studying, updates and optimizations we finally started building the game environment (January 2016).
Defining the costs and the workflow pipeline
With the few data I had on game design (which wasn’t finished and it would have been modified a lot of times during the production), I estimated a data budget for art production that could match production requests and our possibilities.
The first things I started working on were:
- the art workflow,
- the far rendering distance (I couldn’t use an heavy fog, since the set was a mediterranean island on hot summer),
- foliage painting rules,
- cheap but solid shaders and textures, trying to keep pixel rendering cost and video memory low as much as possible.
Since we were three artists working on a such big project (a number reduced to two later, plus a Character Artist) I choosed with no doubts to use Substance for our texturing pipeline. I setup a library to share materials and textures I made with Designer with my other team mates, so they could fetch base materials and use them on Painter quickly.
After defining content rules for artists in Unreal Engine, I’ve also created a library of generic tiling textures in Unreal Engine 4, to fastly texture a no-hero prop using the materials I did setup later.
The generic master material I setup had some really common but useful functions for artists to add features to Painter exported textures.
In case of a fast needed prop with just tiling textures, an artist could create a new instance of the master I created, choose a texture from the library (or the Painter exported ones) and take advange of its parameters and functions like adding mesh normals/ambient occlusion baked from an high poly, or adding a Z projected texture (moss or dust for example), using world aligned on the textures, using a detailed texturing on a top texturing layer, add a fresnel and so on..
I created various type of this master for different types of materials and we finally got different ones to cover several texturing techniques:
- Base_BCR: non metallic materials.
- 1 x 32-bit RGBA texture for Base Color(RGB) and Roughness(A),
- 1 x 24-bit RGB texture for the normal map.
- Base_BCRM: metallic materials,
- 1 x 32-bit RGBA texture for Base Color and Ambient Occlusion,
- 1 x 32-bit RGBA texture for roughness(R), metallic(B) and an optional mask in G channel,
- 1 x 24-bit RGB texture for the normal map.
- Base_Masked: non metallic materials which uses RGB masks and tiling textures,
- 1 32-bit RGBA texture for the masks and maximum 4 textures from the library for the texturing (1 for each mask + 1 base texture).
An example of a static mesh textured following this shader rules, a huge bridge made by tiling textures and masks.
(Made by Claudio Rapuano, Foliage Artist at Indiegala)
- Base_Cloth: as BCR but with different shading setup to be a cloth material.
- Base_SSS: as BCR but with different shading setup to be an SSS material, plus occasionally a texture for SSS masking.
And I can’t forget the Master_Foliage I realized to handle all the foliage shading.
After noticing that we were rendering tons of tris in the scene, me and my colleague Claudio started generating LODs for the heavier foliage and architectural assets, using Simplygon, which saved an enourmous amount of time.
(Speedtree trees lod generation have been a bit tricky, since Simplygon LODs didn’t work always proper on them and generating them on Maya meant losing the ST wind information, which was really important for us.
In the end we choose the Speedtree ones plus an imported BillBoard that was used within a shader to turn it on based on character location.)
Creating the world layer structure wasn’t even really easy, I’ve created the base of a persistent world with streamed sub-levels, choosing how to divide every asset related to design and art, leaving the logic assets to my colleague Matteo Battolla (Lead Programmer @ Indiegala).
Everything had to be loaded smartly around the character avoiding wasting of video memory (textures) and CPU usage (culling), so we handled every sub-level streaming distance to load it just where needed.
We divided each type of game elements in different sub-levels, some examples:
- Landscape: the different tiles of the landscape.
- Gameplay levels: they contained part of the game logic blueprints, the assets which had to have their position saved during level unloading and movable gameplay meshes. An example of a couple of levels in world composition:
- Static lightened scenes: I used this levels to create the environments that needed static lighting, so I could handle their level streaming and keep out of the video memory lightmaps when I didn’t need them. At the same time I could work in parallel with the design guys. Sometimes I used to divide a static lighting level in multiple levels if the map size exceeded 150 mb to avoid CPU peaks on loading. An example:
And so on…
Rendering distance and optimization: how to survive the monster
Even if we defined a lot of rules for art and design, the game was changing continuosly, adding design elements and even new levels in shipped areas, so we were always careful about the game GPU and CPU usage, trying to reduce it every time it became worst.
This was one of the worst view in the game, lots of stuff and long view sights from here:
Landscape Lods was our first framerate boost.
I’ve also managed to have the trees we placed with the foliage in the exactly world position they were in the non-lod tile: this feature wasn’t provided by Epic Games, so I did something.
Every time the designers completed (with the Level Artist) a tile of the landscape, I was ready to generate a LOD for it. Unfortunately, the first thing I discovered was that the foliage wasn’t present in the LOD, and I couldn’t have the landscape popping trees ruining everything. So after some days of trial and error (because basically I didn’t have any idea on how to copy the foliage from my level in the Main World to the LOD level, neither the guys from Epic had it) I magically found a solution, that I can’t share with you for the moment but I will in the future if this feature won’t be added to the engine.
Since that most of the levels were towers or big houses and exploration was one of the design core elements, I had to find a way to render them at anytime, also at 2 km of distances if needed: obviously not loading the level itself with all the gameplay logic and the assets
I used HLOD to render just the exterior of a level at far distances, but beyond certain distances I had to unload the level and this led me to lose the HLOD from the scene.
So I designed a three-layers rendering of the levels (let’s take for instance a “tower” level):
- 100-150 meters from sub-level bound: tower sub-level is loaded + HLOD. Internal meshes are culled.
- 300-400 meters from sub-level bound: tower sub-level is unloaded, HLOD mesh is placed on tile level, with a min draw distance similar to the tower sub-level streaming distance, to pop in just one moment before tower sub-level was unloading.
- 500 meters from sub-level bound: landscape tile sub-level switch to landscape tile lod sub-level, so HLOD mesh placed on it wasn’t visible anymore. Placing the HLOD mesh also on the lod level solved this, allowing me to render it from very large distances.
The next thing to analyze was the foliage: since artists respected the rules and data budgets I gave for its generation, foliage wasn’t a problem and didn’t require a lot of optimization during the development. We just used to tweak transparency when it was breaking the overdraw and draw distance.
Further on we noticed that every layer painted on the landscape had a substantial costs, so I defined a max number of layer per landscape cluster, which were analyzable through the “Layer Density” and “Layer Usage” visualizationd modes.
Static meshes issue was in their quantity and rendering distance: every landscape tile level had 1000-2000 static meshes on it, and we couldn’t handle one by one from the outliner to decide where and how to render them, it would have required a lot of production time, so along with the Lead Programmer I built two different blueprints to handle them:
- CullDistance Assigner: a blueprint which assigns the draw distance to every asset in the level based on datatables which contain all static meshes of the game as a key, having as value a draw distance float based on its bound diameter.
- CullDistance checker: it was for a hybrid manual use. It returns the list of the static mesh actors in the scene with a draw distance of 0 (unlimited): clicking on the inspector near to the name, the users was taken to the mesh position and set a draw distance.
Then I moved on textures streaming: the ListStreamingTextures command returned an unordered and confusing list, so I asked my colleague Programmer Vladimir to edit the engine and make the command return a CSV, then I wrote a python script to order this list based on a texture filter and size and finally I had the possibility to analyze all the textures during the game in a better way, avoiding wastes and resizing when needed.
Another tool I often used during the development, and that has been very useful to analyze shipped builds was Intel GPA and its Frame Analyzer, that allowed me to detect the heavier elements and profiling them deeper than I could in UE4.
The last topic of this article is about the lighting technique I used for “Die Young”.
The game environment had to be illuminated and colored in two different ways:
- a dynamic lighting cycle from morning to sunset with an intense and saturated coloring palette to bring out mediterranean colors in the exterior;
- underground places with a dark and creepy color palette to scary the player, with horror elements: a complete different feeling.
The first solution I chose for the exterior was completely dynamic, with distance field shadowing. We discarded distance field AO because it was too much costly in terms of rendering and production times.
For the interiors, I initially tried some dynamic lights, but it was a disaster in terms of quality/performance ratio, expecially with a dynamic skylight which illuminated everything (since we didn’t use DFAO).
The problem itself wasn’t the creation of an illuminated environment, but how to delete the skylight contribution in those areas, and after some tests (postprocessing, a blueprint to scale skylight intensity, DFAO again…) I decided to use a stationary skylight and bake a shadow map in those areas. This was the cheap solution to keep the game running smoothly and with nice graphics.
Once I got a good shadow map, I illuminated each area with movable and static lights, keeping map sizes below 150MB to avoid loading peaks.
This solution also implied adding some rules for the designers and artists, like avoid landscape in those areas (really bad performance with movable lights, I solved this using lighting channels later, and bad quality with static lighting) and to use bigger modules as possible.
Where they couldn’t use bigger modules, I used to merge everything possible to avoid lightmaps for hundred of modules and save hours of building time.
Since we divided gameplay logic from level design, lighting re-build is only necessary if the designer have to move some modules cointained in SL levels.
In this way designers can edit the levels using the movable gameplay stuffs (movable crates, AI, falling platforms, traps..) which is illuminated by the indirect lighting cache I built along the level.
To reach a good ratio between quality and performance, I set rules for lightmap size of every asset in the game, and avoided using shadows on static lights, since the resolution wouldn’t be enough to have a smoothed shadow baked on a lightmap (for shadows in the interior I usually made use of movable lights).
As I said, each sub-level size is below 150 MB to avoid loading peaks, and frame rate in this areas is totally ok (70+ fps with a GTX 970 on epic settings.)
This solution was then applied to each sub-level with static lighting in the game.
The final result
After two years of hard working we finally got a good product with a nice compromise between graphics and performance.
We squeezed the engine to optimize everything as much as we could, and more optimizations are on the road map ready to be introduced.
The game is an action parkour, and to keep it over 60 fps we discarded a lot of cool graphics feature that UE4 has to make your game graphics really cool and unique, but you know, nothing is free and it’s always about finding a balance.
In the end I can say that it was an amazing experience and I have to make compliments to all my team mates. You rock!
Die Young is available on Steam
During Die Young production, due to issues I had with the project manager (that I will not explain on this article) and a great job offer, I moved to Ubisoft Reflections to work on The Division 2 as a Technical Artist.
Die Young Project Manager decided to change credits after few months, as production obviously evolves and things change, swapping rules and putting me in the very end of the queue, deciding that my work was only classifiable as Environment Artist.
I hope you enjoyed the article.
Thank you for reading.