XaiJu
Kruithne
Kruithne

patreon


Behind the Scenes: Rendering Azeroth

Chapter 1: Introduction

In my last guide, I delved into the process of importing an entire continent from World of Warcraft into Blender using wow.export.

Patreon Post - WoW 3D: Importing Continents 

After releasing that guide, I found myself wondering if I could take it to the next level and render the entire world (of Azeroth). As it turns out, the answer was yes.

Unlike my normal step-by-step guides, this post is going to serve more of a documentary to provide insight on how I achieved this. If you're interested in doing it yourself, you should find everything you need here.

16k render → https://www.kruithne.net/home/files/renders/azeroth_16k.png
8k render → https://www.kruithne.net/home/files/renders/azeroth_8k.png
4k render → https://www.kruithne.net/home/files/renders/azeroth_4k.png

Twitter → https://twitter.com/Kruithne/status/1402758597055074305
reddit → https://www.reddit.com/r/wow/comments/nw8zhx 


Chapter 2: Planning

Before I started throwing continents about, I needed to decide what was going to be rendered, and more importantly: how. The first thing to decide was the terrain.

Pulling in the terrain geometry without any optimization is possible, but it quickly reduces the remaining budget for the render. Since terrain can be heavily optimized without losing definition, I opted for a 0.05 ratio as covered in the previous guide.

Terrain texturing was a little more complicated than it first appeared. On the face of it, minimap textures seemed like a perfect solution: not only are they a low file-size, but they're pre-rendered from the game, meaning they include lots of world detail baked on.

The problem with minimap textures arises when you have floating world models. Since minimap tiles are rendered in a top-down orthographic pass over the game world, they end up painted onto the floor.

I wasn't content enough to just leave these painted on the world, but the advantages of minimap textures were too strong to ignore, so for now I stuck with them and found another solution for this problem which I cover in Chapter 8.

While minimap textures are great, they're still 2D. For small details that doesn't matter, especially from a distance, but with the bigger models such as towers or cities, they appear far too flat.

With the goal of having as much detail in the final render as possible, I did a test with Darkmoon Faire to see how much of an improvement 3D world models would be.

As it turns out, the answer is a lot. My original plan was to include just the largest category of world models, but after this test I decided that for the best outcome I'd want at least 90% of all world models on the entire map.

In the continent guide, we looked at ways to optimize world models by reducing their mesh and baking their textures to a smaller image. For this render, I decided to do neither of these optimizations.

Instead of reducing the mesh of any world model, I instead leaned heavily on instancing as the primary optimization. This means that if you have 800+ of the same model in the same scene (which was more common than you'd think), they'd all point to the same block of data in memory, just rendered at different locations/rotations/scales; more on this later!

For world models, baking the textures would have actually turned out to be a massive performance loss, not a gain, due to diminishing returns.

Once you get to the point of having hundreds (let alone tens of thousands) of world models in a scene, they're sharing a lot of textures by design, but baking each one to an individual image negates that, not to mention the quality loss (unless you bake at a high resolution which defeats the entire point anyway).

With a rough plan in mind, I needed to figure out how to render it. While I'm lucky enough to have 24GB of VRAM, it wasn't going to be enough even with the aforementioned optimizations.

The first prototype for rendering was to import each tile one at a time, render it, and then clear the scene and repeat. Once all tiles were rendered, they would be composited together.

The advantage to this method is that only one tile at a time would need to be imported. With this prototype, I even intended to drop geometry optimization and use full-resolution terrain, since each tile would be free to use whatever resources it needed.

However, the disadvantages quickly stacked up. The first minor issue visible in the above prototype is the seams between tiles caused by interpolation.

The second complication(s) comes from world models. To start with, larger world models are often referenced on multiple tiles. Rendering them multiple times would be bad, so some method of assigning the "correct" tile to render a specific world model would be needed. That becomes tough with world models such as Stormwind City or Boralas, due to the immense overlap and clipping.

I considered rendering all world models on a separate render pass and then compositing them with a mask based on the terrain z-depth, but this isn't exactly trivial either.

Additionally, a lot of tiles actually required special attention, such as the wall of Zul'gurub, and in some areas tiles needed to terraformed, which can't be done well one tile at a time. This complication alone was enough to rule out the per-tile rendering prototype.

Scrapping this plan, I decided the best plan would be to pull in each continent as a whole, prepare it as necessary, and then unload it to free resources. I'd then set up render regions for each continent, unloading/loading continents between each region.


Chapter 3: Layout

With a rough idea of what and how in mind, I now needed to look at where. Since each continent would only exist in the scene by itself, I couldn't just import everything and then move it around as necessary, I needed to position everything without seeing it.

To do this, I took inspiration from how I arranged the maps for Where in Warcraft and created 2D planes for each of the continents which I could then arrange like a puzzle.

The most important part of this stage is that the planes for each continent had to be exact to their final size. To achieve this, I started by exporting the maps from the game, I was going to need them eventually anyway.

With the map exporter, I selected just the tiles I wanted for each continent. For some continents such as Northrend, I selected all of them. For others, such as Expansion01 (Outland), I just needed Ghostlands/Eversong and Azuremyst/Bloodmyst, so I selected them separately and exported them.

To be efficient with time, I also exported world models (WMO) and minimap textures, since they'd be needed later.

For each map that I exported, I then used some Python I wrote to calculate the minimum/maximum bounds of the map (based on the exported files) and create an accurately sized plane for it.

View source code of continent_frames.py on GitHub. 

An important part of this script was keeping the origin of the plane at 0, 0 so that when we later import the actual terrain tiles (which have an origin of 0, 0) they can be easily parented to the plane to inherit the translation offset without any further work required.

Another step taken in the script is to include the tile min/max boundaries as well as the map folder name as custom properties on each plane, this way they can be referenced quickly later in other scripts.

Repeating this process for every continent that was going to be included, I was eventually left with a scene filled with game-accurate sized planes.

The next task was to arrange all of the continents as close to canonical accuracy as I could. To start with, I brought in The Sundered World map from Chronicle Volume 1 and put it beneath the planes, using an orthographic view to align them as close as possible.

Naturally, a lot of things are not to scale, but that's not the aim of this render. In addition to this map, I also used this beautiful map composition by Sub-Thermal to assist with further placement referencing.

Originally, I intended to include Seething Shore as part of this render, but the terrain for the map was such a mess that I decided it was too much work for very little gain, and thus it was axed.

Being able to place the continents in 2D in this manner was great for quickly prototyping the locations, but I needed something more accurate. Since some maps were going to overlap, I needed to make sure they overlapped properly.

For this, I turned to Marlamin's map viewer available on wow.tools, which conveniently includes an ADT grid overlay.

Using this overlay, I was able to measure just the sections I needed and take low-resolution screenshots straight from the browser and slap them onto the continent planes.

At this point, I could see exactly where parts of the map were going to render, so I could polish the arrangement to be a lot tighter and accurate.

In addition to placement, I also set up a camera in the scene to see how everything would be captured together. Since continents would only be loaded one at a time, it was vital to get this positioned first.

In this screenshot, I also temporarily applied rotation to The Wandering Isle and scaled up The Maelstrom to see what they might look like in the final render. These translations needed to be done later - after they're imported - but it didn't hurt to preview it.


Chapter 4: Importing Terrain

The next stage of the journey was to import the maps. Since I was still experimenting with the overall project, I started with Darkmoon Island which became a test-bed for a while.

Importing the tiles one at a time would take far too long, especially in the larger continents with thousands of tiles, so I once again turned to some Python scripting.

View source code for import_continent_geometry.py on GitHub. 

This script was responsible for a number of things. The first thing it does is grab the min/max boundaries and map folder name of the selected continent plane which we earlier stored as custom properties.

With those values, it iterates over every single tile and makes a call to the wow.export Blender add-on (bpy.ops.import_scene.wowobj) to import just the terrain, no world models yet.

For each tile, the script then runs over the same process that I covered in detail in the continent importing guide. This includes setting the UV extension, fixing the UV offset, merging double vertices and finally applying some geometry optimization.

Once all the terrain geometry for a single continent was imported, I'd move onto importing the world models and resolving other complications.


Chapter 5: The Ocean

It may not have escaped your notice, but the ocean is not the same on every map. The default ocean colour is a nice midnight blue, but it's not consistent across all maps.

This poses a complication when all the maps are going to be rendered on the same scene. The first prototype solution I came up with was to create a mask texture based on the aspect ratio of the continent.

I originally wanted to merge the terrain tiles together to apply the mask straight over the top in a shader, but Blender simply can't handle the larger continents being joined.

Instead, I placed the mask on a separate plane and slapped it over the top of the continent, as shown below.

The same mask material was then added to each of the individual tiles. Using UV data projection from the overlaid plane, a secondary UV was created on each tile which is then used to mask the visibility of the terrain by mixing in a Holdout texture.

Using this method, I was able to simply paint away the terrain I didn't want to be visible, without having to edit the geometry itself, only using a single low-resolution black/white image, with real-time results.

In addition, I also used the trick shown in the previous guide to bring sub-sea-level geometry up to sea-level, flattening out the ocean drops.

At this time, this seemed like a good solution given the complexity of the task, but after applying it to a more problem map, it didn't look quite as neat due to the harsh contrast.

After a good nap, I decided to ditch this method of manually painted masks and instead created a shader which uses the depth of the actual geometry to dictate what is visible.

With this method, I was able to finely tune the ranges at which geometry was visible, making it so that the ocean harshly dropped in visibility as soon as it entered deep ocean (fatigue), but having ocean trench geometry still partially visible for a slight gradient.

I was super happy with the results, not only because it looked a lot better, but because it also saved a lot of time that would have been spent trying to paint neat masks around each continent.

The end result was good, but this shader needed to be applied to every single tile. That's not something I wanted to do manually, so I extended the geometry importing script from above to handle this automatically too.

View source code for import_continent_geometry_ex.py on GitHub. 

Happy that the ocean was handled, it was time to tackle the next task!


Chapter 6: World Models

I decided early on that I wanted to include 90%~ of the world models to give the map proper height and detail, but you might notice I didn't import any with the terrain tiles.

The reason for this is that it was important to ignore as many of the world models as possible. Things like Gnomergan and other underground interiors are large models, but entirely not visible in the final render. In some cases, there were entire sections of maps hidden behind mountains, saving us thousands of models.

So how did I import "just the right models"? To do this, I leaned on the same method as shown in my continent importing guide: markers!

Using more scripting, every world model on the entire continent was added to the scene as an empty marker, allowing me to see the placement of everything without having to import them.

This was done using the model placement CSV from wow.export. To save time later, the model path for each was stored as a custom property on the marker. Additionally, the markers are rotated/scaled as necessary already.

View source code for import_wmo_markers.py on GitHub. 

With the markers visible, I spent some time going around and selecting the ones that I wanted to import, often in large selections, once again using scripting to automate.

View source code for import_selected_markers.py on GitHub. 

This script takes the selected markers and imports the associated world model with some important optimizations, the most important being that for objects that already exist in memory, a linked copy of the mesh data is created, rather than re-importing it.

With this method, I was able to rapidly bring in world models in an efficient manner, adding a massive amount of fidelity to the map. As I mentioned in the planning phase, absolutely no down-scaling, baking, or decimation was done to the world models.


Chapter 7: Missing Models

Bringing in the world models for each continent made such a dramatic improvement to the overall look, and it was almost impossible to see any flat structures; almost.

On maps, M2 models are generally small detail props such as fences or barrels. For obvious reasons, I didn't want to include M2 models - there would be millions.

In a number of places across the continents, there were M2 models that were big enough to show up clearly on the minimap as if they were world models.

It would have been extremely impractical to go back, export every M2 model, load in the markers and then import just the single prop that might be needed, so I resorted to an old fashioned method: manually identifying and importing the models.

Overall throughout all the continents, this only happened a handful of times. Aligning the models using the minimap and in-game screenshots was often very quick.

A big thank-you to Falerian, Insaru and Chimpalot for helping identify and find some of the necessary models. 

This method worked fine for all the edge cases that cropped up and took almost no time to do a complete pass, but then along came Zul'Gurub.

Unfortunately, the gigantic walls for the troll city are M2 models, and it's hard to ignore them with how awful they look projected onto the terrain like this.

While it would have been possible to go through and manually place the walls, there's actually a few different models that make up the walls, and getting the placement accurate was important for me.

Instead, I ran another export this time with just the six tiles that contained the walls, with M2 models enabled, giving me a CSV file containing them all.

The plan here was to import markers for the M2 models, but I don't want to bring in a flood of thousands of markers I wouldn't use. With a little bit of searching, I found that the walls were constructed of similar models named stranglethornruins.

Keeping that in mind, I created another script similar to the WMO marker one that imported M2 markers from the relative CSV files, but only if they contained that specific phrase.

View source for import_m2_markers_filtered.py on GitHub. 

Running this script brought in just the markers I needed for the wall. Using yet another script, this time a re-tailored version of the WMO importer, I selected just the relevant markers and imported those.

View source for import_m2_marker.py on GitHub. 

The next edge-case on the tally of missing models comes in at Twilight Highlands with the Iso'rath world model (wiggly boi mouth hole).

The dormant tentacles on the ground are fine, however if we take a look at a screenshot from the game, we can see that there are also meant to be a number of large tentacles flopping about.

This specific case was a little more interesting than the others because those are actually NPCs, not objects. Finding the model for the NPC was trivial, since it was conviniently named creature/tentacle/tentacle.m2.

Despite this being an NPC with animations that I could have used, I just imported it as a static OBJ model as shown above. Rigged models with animations are quite expensive, and the final render is not going to be animated so it would have been wasted resources.

At the same time, I didn't want stiff tentacles, so I opted for a quick-fix and linked each tentacle to a simple curve. This allowed me to bend and warp each tentacle freely as if it thrashing about with almost no overhead.

Despite the attention to detail here, it actually ended being barely visible in the final render resolution, but I rest happily knowing it was there.

Another interesting edge-case was The Wandering Isle. The head/flippers of the turtle are not world models, which is fine as I've already covered bringing in M2 props, but additionally they're also sub-merged under water.

To give a proper effect to these models, I re-used the terrain shader demonstrated in Chapter 5 with some tweaks to the values. I was quite happy with the end result.

Throughout the project there were plenty more interesting edge cases that required special treatment, from the tornado in Darkshore to the titanic disc in Dazar'alor, so I'm not going to list them all; this chapter detailed the most interesting and unique cases.


Chapter 8: Fixing Minimaps

Using minimap textures for the terrain provided some amazing advantages, however as mentioned in Chapter 2 it did come with some problems, especially around floating world models.

Since minimap tiles are rendered from a top-down orthographic view of the game world, world models are printed flat onto the terrain. This is fine 99% of the time, as the relevant world model will cover it up, but as shown this is not the case with floating models.

To fix this, I exported both the minimap and baked (at 1k resolution) terrain textures from wow.export for the problem tiles.

The baked textures come with a lot of issues, so simply replacing the problem textures wouldn't work. There's artifacts in the snow, missing water, and no detail props.

Another reason why I couldn't simply slap baked terrain textures in the problem spots was minimaps are rendered with global lighting, which creates a strong seam between tiles when swapping to baked textures.

The solution I ended up going with was to take the minimap textures for the four problem tiles and lay them out together in Photoshop. I then placed the baked versions over the top and masked them together.

This allowed me to simply paint the world model away without creating harsh transitions between the two texture modes. Once done, I split the tile back up into four separate textures and loaded them back in Blender.

The before/after speaks for itself in this case, especially when the world model was covering up unique terrain such as these floating necropolis' in Zul'drak.

Unfortunately, this solution doesn't work perfectly in every scenario. In Broken Isles, both Dalaran City and Acherus are floating above ocean, and with baked terrain there's no ocean to be seen.

To solve this, I started out by erasing the world model by masking the baked terrain over the top just as I did before.

I then placed a #001d29 (the colour of the sea on this continent) overlay to just the baked terrain layer with the blending mode set to Hue with an opacity of around 65%.

Repeating the process again, but this time with no blending mode. The same as before, this layer was linked only to the baked terrain layer itself.

This was close enough for now, but the bigger issue is that it was covering up the islands that weren't part of the ocean. To fix that, I created another clipping mask that was applied to both the colour layers and painted over the islands.

I spent some time adjusting the levels to match the blue closer, but even with a slight difference you really can't tell from the distance of the render.

This solution turned out to be quick and effective in every scenario that it needed to be used, which was thankfully not that many.


Chapter 9: Terraforming

A number of locations used for this render have areas that were never intended to be seen, which means there were considerably messy when it comes to being included in the render.

The biggest problem for this was merging Ghostlands/Eversong Woods onto the top of Eastern Kingdoms where it should canonically be.

In the image below, I've aligned both of the continents together using Quel'Lithien Lodge as a point of reference since it existed on both maps, and deleted a lot of the dead space.

The overlapping terrain was only a minor set-back, however Stratholme and Deatholme completely interlocking was a much bigger challenge.

For this task, there was no magical solution or automated script that solved everything. Instead, I spent an hour and went over the terrain, carefully either deleting or adjusting everything until it all fit together nicely.

For the larger portions of the mountain ranges, that wasn't too difficult as there was a lot of dead space to work with, which could be re-purposed as necessary.;

Where Eastern Plaguelands overlapped heavily in Ghostlands, I carefully terraformed into a more gradual descent, allowing the zones to naturally blend together in a way that didn't obstruct any visuals of prominent landmarks such as Windrunner Spire or the famous Quel'thalas ruin.

For Stratholme and Deathholme, I didn't want to delete either of them, so I settled for some overlap, however adjusted some of the layout to prevent direct clipping of the two.

Finally, the last problem in this area was the mountain terrain surrounding Zul'aman, which was unfortunately just a giant drop into the abyss.

Once again, nothing special beyond careful terraforming with transform tools, resulting in a hopefully decent enough end result for the render.

Across the other side of the world, another issue presented itself in the form of Silithus. Recently in the narrative, Sargeras stabbed the world with a really big sword (spoilers), but if we look at Silithus, it's not here.

The reason for this is they used technology that was created in Cataclysm for Gilneas in that specific ADT tiles can be used from a different map depending on phasing. This means the "new Silithus" actually exists on another map called SilithusPhase01.

I could have left this out, but I figured it was a very prominent landmark to have included. I mean, it is a giant country-sized sword after all.

Fixing this was actually quite simple. I included the new map of Silithus as it's own continent and deleted the same amount of tiles from Kalimdor.

The ADT tiles fit seamlessly together (otherwise the technology wouldn't work in-game), so slotting them together was as simple as matching the continent offsets together.

Done! Now the only thing left to do was import the world models the same as I did with any other continent. Naturally, the sword had to be complicated and wasn't a world model, but Chapter 7 has us covered for these scenarios.


Chapter 12: Rendering

After a week of carefully constructing every continent, it was time to render. As I mentioned in the original planning phase, the idea to render this was to do it in multiple passes with each pass only having one continent loaded at a time.

Between every render pass, everything in the scene was unloaded and only the content for the next target continent was loaded in. Once every continent had been hit with a render pass, they were composited together.

That's really all there was to rendering; render regions and some basic composting. The dead space was filled in with sea-colour afterwards.

For the subtle grid effect that was added to the final image, I just rendered a 16k grid using the same perspective as the continents and blended it over the top in Photoshop.


Chapter 13: Frequently Asked Questions

Hopefully this post gave you a lot of insight into how I rendered the entire world of Azeroth, but just in-case, I've gone through the social posts and found some of the questions asked and compiled answers here.

Q: Where are all the trees?
A: I considered including the trees, but they would have blocked at least 80% of the landmarks that make Azeroth what it is, so I opted to see the world through the eyes of an orcish visionary.

Q: Why is Teldrassil not on fire?
A: In the new phase of Darkshore, Teldrassil doesn't actually exist. The terrain has been squished down. If you swim down to the ocean floor, you'll actually find the old terrain textures still painted on the ocean floor.

The burning Teldrassil that you actually see in-game today is a giant 2D billboard, which wouldn't have looked too great with the angle of the render I was aiming for, so I stuck with the good old tree stump version.

Q: Isn't The Wandering Isle meant to have eight legs, not four?
A: That's my bad. Let's just pretend they're submerged...

Q: Why isn't everything canonically scaled?
A: Although I focused a lot on trying to position things canonically, I wanted to keep things at the in-game scale as I thought it would be cool to see how everything compared in that way. The only exception to this is the Maelstrom, which felt so incredibly small that I ended up scaling it up to 5x.

Q: Why is Darkmoon Faire positioned south-west of Eastern Kingdoms?
A: I couldn't find a definite canonical location for it, and there was plenty of empty sea there so that's where it ended up. Looking back, I should have put it closer to EK.

Q: Why did GM Island not get added?
A: It doesn't exist canonically, but I did consider adding it. In the end, I just forgot.

Q: What happened to Nazjatar?
A: I fully intended to include Nazjatar. You can actually see it on some of the planning screenshots I posted earlier in this post. In the end, I wasn't happy with the results I was getting and despite having some solutions in mind, I didn't want to spend any more time on such a minor location.

Q: How long did this take to render?
A: Rendering the continents at 16k took around 1-2 minutes per continent. Around 10 minutes total was spent actually rendering. The most time intensive part was importing/processing the continents, which took between 1-3 hours per continent.

Q: What hardware was used to render this?
A: This was rendered on my own personal machine, which at the time of rendering has an NVIDIA GeForce RTX 3090 (GPU) and AMD Ryzen 7 3700X (CPU) with 32GB of Corsair Vengeance DDR4 3200 MHz (RAM).

Q: Do you plan to render other places such as Draenor or Outland?
A: Potentially, yes. I think seeing a full render of Outland would be really cool.

Q: Do you plan to release the .blend file for this?
A: No, sorry. The .blend file alone is 20GB with over 70GB of linked data files. Hopefully this post complete with all the scripts I've included, along with my recent guide on exporting continents gives you everything you need if you want to try this yourself.

Q: Are there any easter eggs on the map?
A: If I told you, they wouldn't be easter eggs anymore.

Behind the Scenes: Rendering Azeroth

More Creators