Website powered by

Tutorial - Adjusting Texture Sizes by Sharpness in Unreal

Tutorial / 20 January 2024

This article describes how to implement a system to adjust texture sizes in Unreal based on the sharpness of the source texture file. Blurry textures without much detail are reduced in size by this system.

The Problem

Very often when working on games you'll come across something like this:


A simple gradient texture using a resolution of 1024px * 1024px. While this texture contains more than a million pixels, the information stored in it is negligible. A 2x2 image could carry the same information with much less data.

Let's consider a less extreme case:

This wood texture contains more information, but if you look closely, you can see that the texture doesn't have fine-grain details. The large patterns are there, but the contrast between different pixels isn't that great. This texture can be scaled down without noticeable loss of detail to reduce the memory usage and build size of your game.

On the other side of the spectrum, you have a texture like this:

The detailed fabric structure and shadows in the crevices are easily lost once the texture is scaled down, so if you're under pressure to reduce texture sizes in your game, this is probably one of the last textures you'd want to touch.

Now, you could go through your entire project and manually adjust the texture sizes to make sure that each texture is only as big as it needs to be for its content, but in a large project that would take forever, and the results are likely to be inconsistent as you have to judge for each texture what texture size is still acceptable without compromising the visuals too much.

So let's see if there's a way to automate this.

Disclaimer

The implementation described below is the result of me trying out some ideas I have had for a while. However, I haven't used it in an actual professional production context, so please take my advice with a grain of salt. If you're implementing your own solution, be sure to tailor it to your project- and team-specific needs, and make sure the results are what you expect. That said, I think the basic principles of this implementation are sound, and some of the techniques used can be helpful in other contexts.

Step 1: Measuring Image Sharpness

The first step is to find a way to measure the amount of information. This is a huge topic in itself. First I found this article on calculating the entropy of images.

At first the results of this technique were quite promising, since the entropy gives you a good measure of the overall contrast in an image. But it quickly turned out that the entropy doesn't care about the frequencies present in an image, so even the gradient above had a rather high entropy, due to the presence of many completely black or white pixels.

Still, the fact that there are so many publicly available image processing libraries for Python was nice to see, so while I had to find another metric, I knew that I wanted to implement the measurement in Python. And very quickly I found another approach to evaluate image quality, by estimating the sharpness of the image.

The Python code used to measure image sharpness looks like this:

from PIL import Image

import numpy as np

import sys


def calculate_sharpness(image):

    array = np.asarray(image, dtype=np.int32)

    gy, gx = np.gradient(array)

    gnorm = np.sqrt(gx**2 + gy**2)

    sharpness = np.average(gnorm)

    return sharpness


if __name__ == "__main__":

    image = Image.open(input).convert('L') # to grayscale

    sharpness = calculate_sharpness(image)

    size = image.size[0]

Step 2: Installing the Libraries to Unreal's Python Environment

Now that the sharpness of an image can be calculated using Python, the next step is to actually use this logic in Unreal. But first we need to install some prerequisites. Since both the pillow library and the numpy library are used by the Python code, both are needed in the Python environment.

Unreal uses its own Python environment, not the one you have installed on your PC. So if you’re not used to working with Python in Unreal, here is how to install any additional libraries for Unreal's Python environment:

  1. Go to your engine folder and there into .../Engine/Binaries/ThirdParty/Python3/Win64
  2. Open the command line by typing cmd in the explorer's address bar and pressing enter
  3. The command python.exe -m pip install ModuleName starts the installation of the module

For the code above, both pillow and numpy are needed.

Step 3: The Asset Action

Now that the detail estimation has been figured out, let's start at the other end of the process, the user in the editor who wants to automatically adjust texture sizes.

To expose options like this to the user, I'm a big fan of Unreal's Asset Action Utilities. Functions in these blueprints can be exposed to the asset context menu and called on multiple assets at once. You can find an introduction to them here.

The function that gets called by the user looks like this:

As you can see, the action only processes textures with the default compression settings. For now, I only want to use this tool on base color textures (see possible future works at the end of this article for why).

A texture with a sharpness of 5 and below will be halved in size, a texture with a sharpness of 2.5 will be quartered, and so on. The 10 is just a magic number, but can be adjusted depending on the desired quality.

Step 4: Calling the Python Code

The next missing piece is the CalculateSharpness function. This function calls the Python code to get the sharpness. The Python code needs the file path of the texture's source file, which is provided by another function that will be covered in the next step. If the specified file path doesn't exist or the file is a .tga file, the function returns early without a result. I added the second check because pillow can't handle .tga files, but there are probably other file formats that aren't supported as well, so you may want to add additional checks if you're using other exotic formats.

To execute the Python code, the 'Execute Python Script' node is used. This node is the only Python execution node in Unreal that allows the use of input and output variables, so it was the logical choice. The only downside is that this node can't be used to execute .py files, therefore the code has to be stored in a literal string.

Step 5: Getting the File Path

The GetSourceFilePath function returns the path to a texture's source file. This is a bit more complicated than you'd expect in Unreal. It can't be queried directly, but is stored in an asset registry tag, along with some other information. Therefore, some string processing is needed to extract just the path, which then needs to be converted to an absolute path:

Using the Action

The action can now be called on any number of selected assets in the Content Browser, allowing you to adjust texture sizes for the entire project with just a few clicks:

Known Issues and Future Work

While the estimated sharpness is a better metric than the entropy, it's far from perfect: Because it’s measured on a grayscale version of the image, it doesn't account for changes in hue or saturation, and human perception has its own quirks: The human eye is much better at detecting contrast differences in dark areas than in bright areas. It's also very good at recognizing patterns, so lines and repeating elements should actually be considered more important than random noise.

And while I'm focusing on base color textures for this article, normal maps and MRA maps do deserve their own customized algorithms: For normal maps, you should measure the angles between vectors instead of luminance differences. For MRA maps, you'd usually prioritize the quality of the roughness channel over the quality of the other two.

Depending on your pipeline, the need to have the source image file to be available may also be an issue, for example if you’re using textures from the Unreal Marketplace or if you're working in a team using version control and the art source files are not part of the workspace you're working in.


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, post them below. To get new posts, follow me on Artstation.

Fixing normals on stretched meshes in Unreal

General / 25 November 2023

The Problem

While I usually avoid scaling meshes non-uniformly, there are situation when you're kitbashing where it can be really tempting. At one point I was stretching a rock quite a bit to create a rock wall, and then I stumbled upon this phenomenon:

Even though the stretched rock clearly had the shape of a wall, the way the surface was shaded implied that it still had a cylindrical shape, to the point where it almost looked like I had stretched the rendered image instead of the mesh.

The Cause

After playing around with other meshes, I could confirm that Unreal does not adjust vertex normals when a mesh is scaled non-uniformly. So while the surface angles change due to the distortion of the mesh, the normals just stay the same.
Searching for advice on this topic, I quickly found this thread:
Non-uniform scaling produces incorrect vertex normals
To summarize: Epic is aware of the limitation and has no intention of fixing it. And I really understand this attitude: Non-uniformly scaling meshes isn't really something you should be doing anyway. Stretched meshes quickly become obvious, so if you really need a mesh with different proportions, it's usually a better choice to just start your 3D software and adjust your mesh in there properly.
If meshes are scaled non-uniformly, it's usually because you're prototyping something or participating in a game jam. So, in most cases, adjusting the vertex normals to compensate for stretching issues would just add extra work for the GPU. At the same time, while the normals in the image above are incorrect, it's not something that many people will notice right away, so if it happens here and there, it's not the end of the world.

Still, it's a compromise, and so it's likely that at some point you'll run into this limitation and want to fix it. So let's do that.

The Fix

The thread linked above already tells us what we need to do: Invert and transpose the object's transformation matrix and multiply it by the vertex normals to get the correct normals.

If you're familiar with Unreal's material editor, you probably already know that it doesn't work with matrices. To use matrices, you basically have to use multiple vectors that make up the matrix. It works, but it feels clunky, and even simple calculations can require a lot of wiring. So I knew from the start that I wanted to use a custom node and work directly with HLSL code.

I also knew I needed the transformation matrix of the object. Finding it was easy enough. You can get the HLSL code for any material from the material editor:

The generated HLSL code contains not only the code equivalents of the nodes placed in the editor, but also a lot of boilerplate.
I quickly found a function called GetLocalToWorld3x3() that returns the transformation matrix of the object. And even better, there's a function called GetWorldToInstance(), which, as you may have guessed, returns the inverse rotation matrix.
One detail that was new to me was the return type: As a long-time Unreal Engine 4 user, I didn't know what a FLWCInverseMatrix was. After looking it up, it turned out to be a matrix for Large World Coordinates, a new feature introduced in UE5 to solve precision problems in very large levels. Since the position of the object is not relevant for this purpose, I knew I didn't really need the precision and could convert to a matrix with regular floats.
The release notes of UE 5.1 already mention a function called LWCToFloat().
So I looked for that function in Unreal's shaders folder and found LargeWorldCoordinates.ush, which contains a lot of helper functions, including
LWCToFloat3x3(FLWCInverseMatrix Value).
So at this point, I had everything I needed. The resulting custom node contains only 2 lines:

float3x3 invMat =LWCToFloat3x3(GetWorldToInstance(Parameters));
return mul(Normal, transpose(invMat)).xyz;

The node has an input for the normal, multiplies it with the inverted and transposed transform matrix and returns the result:

Results

Let's take a look at the stretched rock mesh from the beginning:

At the bottom, you can see the the same stretched rock again. In the middle, you can see the rock with the adjusted material. The difference is immediately obvious. The lighting is now much more even and really matches the shape of the mesh, which is now essentially a wall.
At the top, you can see another version of the mesh. This mesh has been stretched in Blender, and the normals were recalculated. You'd probably expect this version of the rock to look identical to the one in the middle with the normals adjusted, but it looks more like a mixture of the other two versions. That's because this rock is still using the same normal map. If stretched the high poly and low poly versions of the rock and baked a new normal map, the result would be much closer to the rock in the middle.

To have a less specific example, here is a comparison of a stretched mirror spheres with and without the fix:

Both the reflections and the positions of the specular highlights illustrate how the normals change in the fixed version.

Performance

The fix described above added 19 instructions to the pixel shader. While this will not cause performance issues in most cases, it is a cost to be aware of and should not be routinely added to materials if your project doesn't need it.

Update

Shortly after I posted this article, Brian Karis, Senior Graphics Programmer at Epic Games, commented on it, explaining that he had toyed with the idea of fixing this problem when Nanite was introduced, but decided against it because a proper fix would require several more changes in other parts of the engine. He also suggested fixing it on the CPU when creating the transformation matrix for the primitive.
https://mastodon.gamedev.place/@BrianKaris/111472135168861147
I'll update this article once I've looked into his suggestions. For now, though, I think the fix described above is a reasonable workaround for people who don't want to delve into engine modifications, but be aware that the process isn't 100% correct, so your mileage may vary.


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, post them below. To get new posts, follow me on Artstation.

Cheap Hex Tiling for every Occasion

Tutorial / 08 October 2023

Note: While this tutorial shows how to implement hexagonal tiling in Unreal, the techniques presented below aren't Unreal-specific and can be used in any shading language or node-based material system.

If you're an artist working on games with vast environments, you've probably run into something like this at some point:

A texture covers a relatively small area, but is applied to a significantly larger surface, making it obvious that the texture is just repeated. There are ways to deal with this issue or at least make it less in-your-face:

  • You can tweak the texture to make the repetition less obvious. Maybe paint out some unique details that only matter up close, or use a high pass filter to even out the brightness across the texture.
  • Use detail maps to display textures with a different scale depending on the distance to the camera.
  • Add additional details like decals or extra geometric details, to break up the surface and hide the repetition.

All these methods don't really fix the underlying issue, there is still a grid-like repeating pattern, just not as noticeable. To really, truly get rid of it, you need to change how you're mapping the texture.
So as a start, let's rotate and offset the coordinates used in each 0.0-1.0 tile:

While the repetition has disappeared, the edges between the tiles are quite obvious. Can we improve this by blending between the individual tiles? Yes, but there is a drawback to this approach: Blending between the tiles necessitates multiple texture samples - specifically, four times, as there are four adjacent tiles at each corner.

At the green dot, only two textures need to be sampled, but at the red dot, all 4 tiles are visible to some extent, so there is no way around sampling the texture 4 times. Surely there is a way to do this without affecting the performance that bad? Let's see what happens when we use a hexagon instead of a square as tile format:

Now, we only have to sample 3 textures at most. Plus, the hexagon structure makes it more difficult to notice the pattern, since there are no straight lines, making it more difficult for the viewer to spot the pattern.

Implementations

Hex tiling is a commonly used and well-tested technique. There are many great resources online on how to implement hex tiling:

  • Ben Cloward made an in-depth video tutorial: https://youtu.be/F7UxUgow4yg
  • When I tried out hex tiling, I used this paper by Thomas Deliot and Eric Heitz as reference: https://eheitzresearch.wordpress.com/738-2/ This implementation even employs histogram preservation to make sure that even with the texture blended multiple times, the contrast remains unaffected.
  • There is also this implementation by Morten S. Mikkelsen that builds on the previous paper but doesn't require additional precomputation steps: https://jcgt.org/published/0011/03/05/

All these implementations prioritize visual quality, and while they aren’t super expensive, the need to sample every texture three times instead of once wasn’t something I was super happy with. Landscape materials are notoriously complex, since they usually blend multiple layers of texture sets and incorporate various features like detail textures, noise overlays and modifications based on slopes or height.
So I wondered if I could make a cheaper version by sampling the texture only once, using dithering to hide the transitions.
While I'll explain below how I implemented this, I want to emphasize that I don't recommend this approach for every situation.
This method doesn't reach the quality of proper blending. Using dithering instead of proper blending is always a compromise, and depending on your quality expectations, proper blending might be the way to go.

Dithering

Whenever you find yourself needing to sample a texture multiple times and blend the results, it's worth considering whether you can achieve the same effect by blending the coordinates using dithering and then sampling the texture with the blended coordinates.

Now let’s try the same with applied dithering on the weights: In this example, a texture is sampled with two different coordinates and the results are then blended.

Now, just for the sake of it, this happens when you just blend the coordinates instead of the textures and then sample the texture once:

Obiously, blending UVs doesn't make sense, but let's what happens when we use dithering to use only one of the two UVs per pixel:

Better, I guess? It already resembles the blended textures, but the transition looks ugly. The reason for that is the mip-map calculation. When sampling a texture, the mip-map to use is calculated by looking at the difference between the coordinates of the current pixel and the coordinates of the previous pixel. The bigger this difference, also called derivative, the smaller the resolution of the mip-map to be used.
But due to the dithering, the coordinates of the previous pixel are completely unrelated and way off, so the texture is sampled as if it were far away from the camera, using a mip map with a lower resolution.
With mip-mapping disabled, the transition area is cleaned up:

That’s not the actual solution to this problem, though. Mip maps exist for a reason (multiple ones, to be exact). Without mip-mapping, the texture can't be streamed in, and is loaded at full resolution.
And if you don't care about memory usage, sampling the texture at full resolution even in the distance won't look great, as shimmering is introduced.
Thankfully, you don’t need to rely on the automatic derivative calculation, you can also specify them manually. In the example below, the derivatives for one of the coordinate sets are calculated and then used throughout:

This works because even though the second UVs are rotated differently, the scale remains the same, so even if the derivatives differ slightly, most of the time the same mip-map is going to be used.
Now let's rotate the derivatives as well:

The visual difference isn’t noticeable in this case, but with flatter viewing angles, not rotating the derivatives together with the coordinates can make a difference. But then again, using dithering instead of actual blending is already a far bigger compromise, so I don't feel bad about skipping this step.

Building the hex tiling material

Setting up the coordinates

After having looked up other implementations of hex tiling, I started building my version using dithered UVs.
The first step is to create three separate layers with coordinates:The TriangleGrid node contains custom hlsl code borrowed from Thomas Deliot and Eric Heitz:

// Scaling of the input
uv *= 3.464; // 2 * sqrt (3)
// Skew input space into simplex triangle grid
const float2x2 gridToSkewedGrid = {1.0, 0.0, -0.57735027, 1.15470054};
float2 skewedCoord = mul(gridToSkewedGrid, uv);
// Compute local triangle vertex IDs and local barycentric coordinates
int2 baseId = int2(floor(skewedCoord));
float3 temp = float3(frac(skewedCoord), 0);
temp.z = 1.0 - temp.x - temp.y;
if (temp.z > 0.0)
{
    w = temp.zyx;
    vertex1 = baseId;
    vertex2 = baseId + int2(0, 1);
    vertex3 = baseId + int2(1, 0);
}
else
{
    w = float3(-temp.z, 1.0 - temp.y, 1.0 - temp.x);
    vertex1 = baseId + int2(1, 1);
    vertex2 = baseId + int2(1, 0);
    vertex3 = baseId + int2(0, 1);
}
return 1.0f
;

I recommend reading the paper to get a better understanding of what this code does. The most important thing to understand is that while the technique is called hex tiling, the shapes that are blended together aren't hexagons, but the adjacent triangles that make up each hex tile

The node returns:

  • a float3 containing the weights of the triangles
  • three float2s containing index values of the hexagon containing the current triangle.

The hexagon IDs are subsequently used to compute random values for the tiles. This is what the hash function for that looks like:

These values are then employed as angles to rotate the coordinates. Afterwards, the angles are appended to the coordinates, since you'll need them later on.
As you can see, in my material graph, there is an alternative to rotating the coordinates, called stepped offsets.

Stepped Offsets

Rotating the coordinates works very well for chaotic, directionless surfaces like grass, dirt or concrete. But as soon as a texture has a noticeable orientation (like wood grain), randomly rotating the tiles will break it. Therefore, I added the option to just shift the coordinates instead. And since there are many cases where textures contain grid-like structures, the offset is applied in discrete. For instance, if your texture consists of 10x10 plates, you can set the Steps_U and Steps_V parameters accordingly so that the UVs are only offset in 0.1 steps, ensuring that the gaps between the plates still align with each other.

This is the Stepped Offsets material function:

Below you see a comparison between stepped offsets on the right and non-stepped offsets on the left:

Combining the Coordinates

Now that we have the coordinates, let's combine them:

The combined coordinates are then used to sample the textures. Remember to use derivatives that are calculated using the original coordinates before the offsets or rotations were applied!

Another important detail: Whenever you rotate UVs, make sure to rotate the normals accordingly the other way around, otherwise they'll point in the wrong direction. That's the reason why the angles were appended to the coordinates earlier.

The Results

Here is a comparison of classic tiling, hex tiling with random stepped offsets and with random rotations.

Since the texture doesn't have that much contrast, dithering works very well in this case, but your mileage may vary. The focus of this implementation was versatility (hence the option for stepped offsets) and speed (hence the dithering instead of sampling the texture multiple times).

It has its limitations though if it comes to quality. I wouldn't recommend adding parallax mapping for example, since it will break in the transition areas. Still, if you've read this article up to this point, I'm hoping I could provide you with some helpful insights that will turn out to be helpful. If you come up with any changes or improvements to this implementation I'd be happy to know about them!


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, post them below. To get informed about new articles, follow me on Mastodon or Bluesky.

You can also find this article on my WordPress blog.

Creating Starry Nights without Textures

Tutorial / 29 May 2023

The most common way to implement night skies in games is to use a nice looking cubemap texture. The result usually looks good. And since cubemaps are a common feature in most engines, it's usually a topic that doesn't get much attention. That is, until you're developing a game for a low-end platform with not much VRAM. Then you suddenly realize that your star texture is 26MB, which you really don't want to spend on a single texture. And since the distance to the sky never changes, there's nothing to be gained by streaming the texture, so it always loads at full resolution. And even though the texture is usually just one color, reducing the texture resolution isn't an option either. Because stars are so small, often covering only 1-2 pixels, they will simply disappear if the resolution is too low.

Since I wasn't really happy with the memory consumption of a cubemap just to display a few white spots in the night sky, I looked for other ways to render stars. In this tutorial I'll show you how to do this by creating a star field mesh and using a material to make adjustments at runtime. This approach doesn't use any textures at all, just a low-poly mesh and a simple material. I used Blender and Unreal for this, but all the features used for this technique can be found in other DCCs and engines as well. As a side effect, this technique also reduces the amount of GPU time needed to render the sky, since there is no cubemap to sample for every single sky pixel. This doesn't really make a difference, though, since the effect is greatest when the player is looking at the sky, which is usually a situation where GPU time is not an issue anyway because there is almost nothing on the screen.

Creating The Mesh

It all starts with a single quad that will be a star. There's not much to do with this quad, except to change the normals so that they point out instead of up:

This will be important later in the material, as you will be able to resize the stars later.
Next, copy the plane a few times and arrange the copies into a hemisphere:

The size of the quads doesn't really matter at this point, so they can be larger than you want the final stars to be.
While this mesh is starting to resemble a starry sky, there is still one step missing: In order to add color and size variation in the material, we need a way to identify individual stars. A random value between 0-1 per star is the easiest way to do this and can be stored in the vertex colors. Since manually editing the vertex colors can be tedious, I used this script by Michel Anders: https://github.com/varkenvarken/blenderaddons/blob/master/connectedvertexcolors%20.py
It assigns a random grayscale value to each mesh element. Similar scripts exist for every single 3d software out there, and I recommend using them, they can be used for so many other things that the initial effort to find them and get them to work is time well spent.

The mesh with the random vertex colors per star

Creating the Material

After exporting the mesh to Unreal, the next step is to set up the material graph:

The material setup in Unreal

There are three critical features in this material:
1. The vertices can be moved along the normals to adjust the size of the stars. This is important because you usually want the stars to be as small as possible without reaching sub-pixel size.
2. The vertex color is used to add random variation to the color and scale of the stars.
3. The vertex color is also used to hide stars. If their assigned value is below a certain threshold, all of their vertices are moved to the origin. This makes it easy to adjust the number of stars without having to go back to the mesh.
And voila, after placing the mesh in a level and assigning the material, this is the result:


Limitations

This way of rendering stars works especially well if you intend to separate other elements of your sky as well. I recommend creating the sky color procedurally in a shader (if you're using Unreal, the SkyAtmosphere system already has you covered) and having your clouds as separate objects, be they billboards, particle effects or real volumetric clouds. If you are primarily working with static levels, but want a lot of details like nebulae or other objects in the sky, the old reliable cubemap approach is probably the way to go.

By the way, if you really want to delve deeper into creating backgrounds with geometry instead of textures, this SimonSchreibt article explains how Homeworld 2 created some stunning vistas without any textures: http://simonschreibt.de/gat/homeworld-2-backgrounds/

Bonus Section

As you probably know, the human eye is most sensitive to light at the edge of its field of view. As a result, stars are often invisible when you look at them directly, but become visible when you look at them from the corner of your eye. You can easily replicate this effect in Unreal by multiplying the star's opacity by the distance from the center of the screen:


Whether you want to use this heavily depends on the visual target of your game: Do you want to emulate the human perception or rather have a cinematic feeling?


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, post them below. To get new posts, follow me on Artstation.

Unreal Tutorial: 5 New Features In Unreal 5 To Improve Your Editor Tools

Tutorial / 16 June 2022

While most people are probably gushing over the visual fidelity of the Lumen system or the breathtaking details possible with Nanite, Unreal Engine 5 has introduced a lot of less spectacular features that are especially useful for tech artists and anyone else who’s creating tools for the Unreal editor. Sadly, these features are often not documented and only very briefly mentioned in the release notes, so it's easy to be not aware of them.

Since I’m currently in the process of moving a lot of editor tools from UE4 to UE5, I thought it would make sense to compile a list of new features in UE5 that make creating and using editor utilities easier. While not as flashy as some other features new in UE5, all these new features or blueprint nodes have the potential to save you a lot of time or nerves when working with the engine on big projects. While you can just move editor utilities from a UE4 project to a UE5 project without any issues, UE5 offers a lot of new options to improve the user experience, and just taking a few minutes to adjust existing tools can make a big difference. 

Color Customization

Since version 5.0, Unreal offers a lot more settings to customize the editor UI colors. This is something to keep in mind when working on custom UIs. Just using hard-coded color values may work with the default values, but can result in a mess if your tool is used by users who changed the settings. This is how my image comparison tool looked with adjusted color settings when I opened it in Unreal Engine 5 for the first time: 

As highlighted in red, several texts are hard or impossible to decipher. Luckily, there is a way to deal with this. The utility widget is in fact aware of the current foreground color, and you can use it for your texts just by enabling the Inherit option on the foreground color setting:

After a few adjustments, the UI is still readable even with heavily edited UI colors:

Disclaimer: I don’t recommend changing the colors of the Unreal editor’s UI to create a light mode. While the new customization options are a big improvement compared to previous versions, there are still a lot of UI elements that expect the default colors and will look weird with any colors adjusted. Still, you want your tools to work correctly for as many users as possible, so it’s a good idea to check how a tool looks even with a rather uncommon setup.

Asset Action Sub-Menus

A very small, but helpful new feature in UE5: Asset and actor actions are now organized in sub-menus according to their categories. If you are an avid user of these actions, the menus in UE4 could get a bit messy:

But in UE5, everything is neatly organized:

So if your actions aren’t organized in categories yet, now is a good time to change that!

Object(s) Dialog

In UE4, when you wanted the user to edit/review assets, you’d open the default editor:

This would work nice for 1-2 assets, but once you want the user to edit dozens of assets at once, it’s not that great to use. UE5 has a new feature for this, the object(s) dialog:

The objects dialog basically combines several details windows into one, allowing you to review and edit the details of several UObjects at once:

Note: While the dialog has the option to confirm or cancel at the bottom, the changes you make in the dialog are applied instantly, even though the interface would make you believe otherwise.

Working With The Content Browser

Another handy new feature in UE5 is the Get Current Content Browser Path node. In UE4, there was no easy way to know which directory was currently opened. To work around that, you could get the selected assets and derive the current path from them, but this solution requires the user to select at least one asset, which isn't very elegant.

In UE5, you can easily get the current path, which is super helpful to iterate over all assets in the current director.

Working With Material Instances

One of the really annoying omissions in UE4 was always the lack of a blueprint node to edit static parameters on material instances. The best you could do with out-of-the-box tools was to get the value and notify the user if it wasn't the expected one.
While there is a helpful tutorial to create it yourself, everyone who prefers to work blueprint-only (or doesn't understand Japanese) will be happy to see that finally, there is a blueprint node for that:

And in case you are curious how the new shader permutation affects the performance, UE5 has you covered. You can now get the complete material statistics you know from the material editor, making it possible to rank your materials by instruction counts or flag all material instances that exceed a certain complexity:


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, post them below. To get new posts, follow me on Artstation.

Unreal Tutorial: Using Editor Utilities to Automate Level Design Tasks

Tutorial / 01 January 2022

Unreal's editor utilities allow you to improve and speed up your workflow by automating repetitive steps without having to modify the engine or learn C++. However, the documentation is lacking, so using them requires some patience to figure out how to use them correctly. So I thought it would make sense to share some insights on how to use them. Since there are many types of utilities, this tutorial only covers the actor action utilities, but everything is also applicable to asset action utilities, which work pretty much the same way.

I'm using UE5 for this tutorial, but editor utilities have been part of Unreal for a while now, so all the steps described work the same for Unreal 4 (Editor Utilities were added around version 4.22).

Actor Actions

Actor actions are available in the context menu of an actor placed in a level:

While they can also do other things, the most common use case for them is to modify the currently selected actors in the viewport. Imagine you have a few hundred objects and you need to set property on all of them, but the property needs to vary from actor to actor or is dependent on circumstances like the distance to another object or the assigned material. Doing this manually could take hours, but using an actor action, it's a matter of seconds and has less potential for error.

The action created in this tutorial rotates the selected actors around the Z-axis by a random amount within a customizable range. This is useful to break up the visible repetition.

Creating the Asset

Editor Utilities are listed separately from blueprints in the asset creation menu. You have a wide range of options when selecting the parent class, but most classes are intended for very specific use cases and can be ignored most of the time. There are two action utility classes, one for actor actions and one for asset actions. They work pretty much the same, except that actor action utilities appear in the context menus of actors, and asset actions appear in the context menus of assets in the content browser.

Specifying the Supported Class

After creating the action utility, the first step is to override the GetSupportedClass function. In this case, I set the return value to Actor, but for most actions, you should specify a more specific class so that the action only appears in the context menu if it supports the current actor. Especially if you use a lot of actions, this helps to avoid clutter in the menus and prevents the users from running an action on a light that is intended to modify a mesh, for example.

Creating the Function

Any function added to the utility that isn't pure and has Call In Editor enabled will appear as an action in the context menu. So if you're creating functions to organize your code, disable Call In Editor. Also, be sure to add a description, it's used to generate the action's tooltip. If you use a lot of different actions, you can also add a category. Multiple actions with the same category will be grouped in the same submenu, making it easier for users to navigate the menus later.

The Function

The function for adding the random rotation is quite simple. It gets the selection set, an array containing all selected actors, and uses a for each loop to add a random rotation to the Z-axis. The range of rotations is provided by a float input. When a function is called that uses inputs, a dialog window opens where you can set the values:


The Undo Stack

To make editor tools safer to use, make sure they use Unreal's undo system. To do this, you need to wrap all steps in transactions. A transaction is a list of steps performed during that transaction and the previous state of modified objects. When you undo the transaction, this state is restored. So if you want to make all your changes undoable, you need to call the Begin Transaction node at the start, the Transact Object node for each object you want to modify, and the End Transaction node when everything is done. 

Actor actions are already wrapped in transactions, but unless you tell the undo system to save object states using the Transact Object node, the transaction is empty and discarded. As you can see in the example function, each actor's state is saved before the rotation, therefore the transaction contains changes and shows up in the undo history. Unfortunately, it's just named Blutility Action. Blutility is the outdated name for editor utilities, but it still shows up here and there in the Engine.

For reference, this is how the function would look like if the action wouldn't already be wrapped in a transaction:

This graph produces the same result as the previous one. When transactions are created during another transaction, they are simply added to that one. If this isn't the case, it makes sense to plug something into the context and description inputs, so that the undo step has a name when it shows up in the undo history.

If you encounter problems with the transactions, the undo history is a useful debugging tool. Just enable the Show transactions details setting and you can see the lists of modified objects and even the properties changed on those actors:

Running the Action

After saving your utility, you can run the action on any number of actors to add some variation:

While this is a rather simple example, actions can perform complex tasks and save you a lot of time.

Maintenance

If you use action utilities a lot, there are a few ways to make working with them easier:

  • Keep all your action utilities (and editor utilities in general) in a folder separate from your game assets.
  • Use a naming convention that tells you that this asset is an action utility. All editor utilities share the same asset type, so without a naming convention you won't be able to tell their class immediately.
  • If you haven't managed to maintain a solid naming convention, you can also search for ParentClass==ActorActionUtility in the content browser.
  • To make editing actions easier, you can press Shift while clicking on an action's menu entry 


If you enjoyed this article or found it useful, please share it with others! If you have any comments, questions, or feedback, please post them below. To get informed about new articles, follow me on Artstation, on Mastodon or Bluesky.

You can also find this article on my WordPress blog.

Tutorial: Adding a PBR validation view mode to Unreal

Tutorial / 21 December 2021

I recently found this Twitter thread by David Torkar, in which he explains how to add custom view modes to Unreal using post-process materials. So I tried it out and added a PBR validation mode, something which I felt was always missing from Unreal.

What are valid base color values?

According to the usually used references, the lower limit for the base color values should be somewhere around 30-50 in a 0-255 sRGB range. The upper limit is usually set at 240. For metal reflectance values (which is technically something else than the base color, even though we use the base color input for it), the range should be 180-255.

While values can technically be out of this range, our goal is usually to replicate reality, and there aren't really that many materials in the real world that are outside of this range. Even when creating stylized content, keeping all colors in a valid range is a good idea to make sure materials behave predictable under identical lighting conditions. If you choose to leave the range, make sure you have a good reason to do so.

The Validation Material

The material is quite simple. Since it's not used at runtime, I didn't bother too much with optimization and added the remapping and sRGB->linear conversion for the range parameters. From my experience, working in the 0-255 range is more intuitive for most people, since you don't have to deal with decimal numbers. There are three colors used to mark values below, in, and above the valid ranges. At the end of the material, the shading model used for the current pixel is checked, and for unlit surfaces, the original color is kept. This helps with the orientation in the level since the sky is still visible this way.

When enabling the material using a PP volume, it behaves as expected. Both the dark areas in the chairs' crevices and the grime on the metallic surface in the background are marked as too dark, the white box is marked as too bright.

Adding the View Mode

Now that the material works as intended, move it to a directory where it doesn't get mixed up with the rest of the game, something like
/Game/ViewModes/PP_PBRValidation_BaseColor.

Now, to add the view mode, open DefaultEngine.ini (located in the Config folder in your project directory) and add a new section:

[Engine.BufferVisualizationMaterials]BaseColorValidation=(Material="/Game/ViewModes/PP_PBRValidation_BaseColor .PP_PBRValidation_BaseColor ", Name=LOCTEXT("BCValidationMat", "Base Color Validation"))

After restarting the editor, a new entry appears in the Buffer Visualization menu:


Other Materials

Using custom post-process materials, you can also display other useful information. These two materials visualize the clear coat and clear coat roughness values, which is useful when fine-tuning car paints:

Known Issues

All materials described above are jittering when used with TAA enabled, since TAA slightly offsets the image each frame. This helps to smooth the image over several frames, but since the PP materials are applied after the tone mapping, they aren't affected by the TAA, which would smooth out the jittering. If it really annoys you, you can just disable TAA while working with the view modes.
You can also change the location of the material to be before the tone mapping, but if you're using any color grading, LUTs, etc. in your game, these get applied during the tone mapping and can change the look of the view mode.
As you may already have noticed, using post-process materials isn't a replacement for proper view modes, since you're restricted to the information provided in the GBuffer. Still, it's a nice trick to be aware of.


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, post them below. To get new posts, follow me on Artstation.
You can also find this article on my WordPress blog.

Tutorial - Disabling obsolete shadow casters in Unreal

Tutorial / 21 December 2021

A while back, I watched this talk about performance optimizations in Gears 5, and I was intrigued by their process for detecting objects that don't cast visible shadows.
Basically, in outdoor scenes, you usually only have one relevant light source, the sun:
Any mesh that doesn't have a clear line of sight to the sun is in the shadow. No light will reach it, so shadow casting will have no visual impact. Therefore, shadow casting can be disabled for this mesh.
Theoretically, you could avoid rendering the shadow for this mesh by adding occlusion culling to the shadow rendering, but in most cases this would probably actually worsen performance, since it's expensive to do this at runtime, and as long as the environment is static (which is the case for many games), it's also not necessary, we can just do it once in the editor.
So, I decided to implement this optimization using blueprints. My method is a bit simpler than the method mentioned in the video linked above and is therefore more conservative, but depending on the level content, it can still improve performance noticeably. And since it worked so well for me, I decided to write this tutorial for other people who are struggling with the performance cost of shadow rendering (and who isn't?).

Use Cases

If you want to add this implementation to your own game, make sure you'll benefit from it. There are some conditions that your content should meet:

  • If you have a day-night cycle in your game, this optimization won't work. You could adjust the blueprint to check if an object is lit by the sun for every sun position throughout the day, but the number of objects that are never reached by the sun is probably quite small.
  • For baked lights, this optimization doesn't apply. Disabling shadows on objects used for light baking reduces the baking time but doesn't improve the performance of the game at runtime. Instead, it degrades the quality of the indirect lighting.
  • If parts of your level geometry are destructible or can move during gameplay, you will need to adjust the blueprint to ignore moveable/destructible objects. If your level is almost entirely destructible, the savings you get from this method may not be worth the effort.
  • If other light sources can be added at runtime (maybe the player can equip a flashlight), some meshes won't cast shadows when lit by them. I imagine it might be possible to split the 'Cast Shadow' setting into two, one for directional lights and one for all other light types, but that's another topic for another day.

While the implementation is mostly blueprint-based, I also made a small engine change to make the tool easier to use. It's not strictly necessary for the blueprint to work, though, so you can skip this part if you're using a precompiled version of the engine.

How it works

To check if an object casts a shadow, the blueprint iterates over all shadow-casting static mesh components in the level. For each mesh, it does 8 line traces towards the sun, using the corners of the bounding box as starting points. If at least one of the traces doesn't hit a shadow-casting mesh, the mesh is lit by the sun and must cast a shadow. Since the bounding box is used instead of the actual mesh, there will be some false positives, so some objects will be considered lit when they aren't. Theoretically, there could also be cases where all traces hit shadow-casting geometry while the center of the object is still lit, but so far, this situation hasn't come up in any of the levels I've used for testing.

Creating the Blueprint

1. Create an Editor Utility Blueprint and select EditorUtilityActor as parent class. This way you can be sure that the actor won't be included in your packaged game. Place the actor in a level that contains some static meshes and a sun to test it on.

2. In the blueprint you need to create 6 functions. The first one is called  'Disable Obsolete Shadows':

This function is called in the construction script and uses the other functions to perform all the necessary steps. First, it gets the sun's light vector using the 'Get Light Vector' function. Then it iterates over all the actors in the level, gets the static mesh components and calls 'Set Shadow Based on Light Vector' on them. The cast to the Instanced Foliage Actor makes sure we don't perform these steps on foliage, since we can't control shadow on individual instances.

Note: As you can see in the screenshot, the light vector is stored in a local variable. All variables in this blueprint are local, which makes it much easier to keep track of which variable has which value at any point in the graph.

3. The 'Get Light Vector' function is quite simple. It finds all directional light source components in the level, and if a component is both shadow-casting and dynamic, it returns its forward vector (The light direction) multiplied by 100000 (which is just a very high magic number).

4. 'Set Shadow Based on Light Vector' checks for each static mesh component if it's lit by the sun and sets the shadow accordingly:

If a mesh component passes the checks, the shadow casters are searched for for each vertex of the bounding box.
If a ray doesn't hit a shadow-casting mesh, the loop is broken and the shadow is enabled.

5. 'Get Bounds Corners' is used to get the starting points for the line traces. This is not the most exciting or elegant function:
6. The 'Is Lit By Sun' function does the actual line traces. Yes, plural, because one isn't always enough. The line trace can also hit blocking volumes or meshes that aren't casting shadows. In this case, we need to do a new line trace, starting at the hit position and excluding the hit actor. This loop is repeated until we reach the end of the line trace or find a shadow-casting mesh. 

Each time a hit is detected, the component is cast to a static mesh component, and if the cast is successful, the 'cast shadow' setting of that component is checked. If a shadow is cast, 'Lit By Sun' is false - at least for this line trace. If the hit component is not a static mesh, the hit actor is cast to a landscape. Technically, the blueprint should also check whether the landscape casts shadows, but for my personal project I'm assuming that all landscapes cast shadows, so I skip this step.

7. That's it! After compiling the blueprint and placing it in a level, you're done. There are a few steps to refine it:
At least for initial testing, you may want to add some logic to the blueprint to count the number of meshes on which shadows get disabled by this logic, to see if the optimization works as intended and saves some draw calls. You can also use the options in the line trace node to draw some debug lines.
Depending on your projects, you may also want to extend the blueprint to also consider spline meshes or even skeletal meshes if you're using skeletal meshes for your environments. You can also extend it to include spotlights or point lights.

The Engine Change

As mentioned earlier, I slightly modified the engine to have a setting for each static mesh component to disable this automatic system. This change is fairly trivial if you're familiar with C++. If you're not, the biggest hurdle is downloading the engine's source code and successfully compiling it. There are plenty of resources for this on the web, so I won't cover it here. Once you have your engine built, all you need to do is open
...\Engine\Source\Runtime\Engine\Classes\Components\StaticMeshComponent.h
In the class body of UStaticMeshComponent, add the marked lines:

The comment in the first line seems trivial, but Unreal uses these comments to generate tooltips in the editor, so don't skip it. Then the UPROPERTY macro is called. This macro makes sure that the variable declared in the next line can be used in the editor.
After compiling and starting the editor, the new setting appears in the static mesh component details:


The Results

In order to have a representative comparison with a level that isn't built in a way that exaggerates the effectiveness of this optimization, I used KK Design's Modular Ruins Kit, available on the Unreal Marketplace. The example level contains several cliffs and ruins that cast shadows on several clutter items on the ground, so there should be some shadows that can be disabled by this system.
These are the results after placing the blueprint in the level:
While the performance difference isn't huge, the optimization saves 0.2-0.3ms on both CPU and GPU. Note that there is no visual difference, and since the system is fully automatic, there are no adjustments necessary later on when the content changes or new levels are added. Depending on your game, you'll likely see consistent performance improvements throughout your outdoor levels.

Known Issues

When using contact shadows, there are some cases where this optimization can make a visual difference. Contact shadows aren't limited by the shadow draw distance used for the CSM shadows, so for smaller objects, they are helpful to still draw at least some shadows in the distance. Contact shadows aren't rendered for non-shadow-casting meshes in 4.26+, so disabling shadows removes these shadows as well.
So if an object is far away from the camera, it's not covered by the CSM shadows due to the restricted draw distance. As a result, the contact shadows missing now make an actual difference. This isn't a real problem in the levels I've used this technique in so far, but it's something to be aware of.


If you enjoyed this article or found it useful, please share it with others! If you have any comments, questions, or feedback, please post them below. To get informed about new articles, follow me on Artstation, on Mastodon or Bluesky.

You can also find this article on my WordPress blog.