May 11, 2021
There are plenty of great tutorials out there that can help you create a circular material in Unreal Engine 4. For example, see Tom Looman's post on creating a circular progress bar with the help of a gradient texture, or a post from Unreal Possibilities showing how to do it without a texture. This post goes a bit further to show you how to chop a circle into segments.
I put this material to use in one of my latest demo projects, Rush Reborn. If you find this guide useful, you might want to check out that project as well.
The first thing we want to do is create a material and make sure our environment is set up so that it is easy to work with.
Right click anywhere in the Content Browser and create a material from the context menu that appears. I am calling my material M_Circle_Segmented, but call it whatever you want.
Every material must be compiled for a specific target domain. You can switch the Material Domain in the Details panel of the material editor under the Material category. I am targeting the
Deferred Decal domain, but the material should work just as well for
User Interface and other domains.
We also need to set the Blend Mode (in the same category) to
Translucent, because we use opacity to define our circle and its segments.
You may find that your material is being previewed against a courtyard scene or some other colorful backdrop. This can make it tough to understand how the material looks while we are drafting it.
Instead, I prefer to work on a black background. To do so, navigate to the Window menu and open the Preview Scene Settings panel. In that window, find the Environment category and:
On a similar note, the circle may be distorted when projected onto a sphere or cube. To remedy this, select the Plane option in the Viewport to preview the material on a flat surface.
The material that we will be building does not make use of any hand-designed textures. This is allows me to work around my general clumsiness with artsy softwares (Photoshop, Paint, etc.). It also serves the dual function of making our materials very flexible, which is great for prototyping. Materials that depend on textures can be harder to iterate on. Finally, because our material is entirely driven by math, it can be scaled up or down without losing fidelity.
At this point, you should have a simple black material. That's rather boring (and possibly invisible against our black canvas). Adding color to our material is trivial.
Simply add a
Color vector parameter to control the color and an
Emissivity scalar parameter to control the brightness. Multiply these parameters together, and plug the result into the
Emissive Color field of the material. We now have the same flat output as before, but at least it's colorful.
The remainder of our work lies in deciding which parts of the material should be visible. As you might expect, the results of the next steps are plugged into the
Opacity field of the material, which dictates the parts of the material that are shown.
Our material will consist of two separate sections: a solid inner circle and a segmented outer circle.
The inner circle is made using the
RadialGradientExponential function. This function outputs a circle that starts at full density at its center and gradually decreases in density the further from the center you go.
We want our circle to be crisp and solid, so we plug in a high constant to the
Density parameter. We also add a
CenterRadius scalar parameter to control how large the inner circle is.
If we connect the result of the inner circle function to the
Opacity material field, we should see just a small inner circle. Effectively, we are saying that wherever the input (the inner circle function) is greater than zero, show the material. Where the input is zero (black), hide the material. This understanding will come in handy very shortly.
The segmented outer circle is a bit more involved, but it isn't too bad once you break it up into smaller pieces (pun intended).
Start as if you were making a solid ring. Create two
RadialGradientExponential functions, and set both of their
Density parameters to a high constant.
Now make one of the circles a bit smaller by changing its
Radius, and then subtract the smaller circle from the larger circle. This results in a solid ring.
To implement segments, we need to use a tiny bit of math. Create a
Custom material function, which allows you to write inline shader code. Call it
In the Details panel for the node, add four inputs:
Origin- The normalized center of the circle (0.5, 0.5)
Coordinate- The normalized texture coordinate (0-1)
Segments- The number of segments to chop the circle into
GapSize- The size, in angles, between each segment
Coordinate parameter should be supplied by a
TexCoord node, which outputs where on a given material the current pixel is located. All other parameters should be set by constant or scalar parameter.
Then, in the
Code field of the node, add the following:
float X = Coordinate.x - Origin.x; float Y = Coordinate.y - Origin.y; float Angle = (atan2(Y, X) * (180.f/PI)) + 180.f; // Angle 0-360 float SegmentSize = 360.f / Segments; float NormalizedAngle = fmod(Angle, SegmentSize); return (float)(NormalizedAngle > GapSize);
At the outset, we calculate the x, y positions of the current pixel coordinate relative to the origin. Then, we feed these positions to the
atan2 function to determine the angle that the pixel sits at relative to the origin. The
atan2 function is preferable to the standard arctangent, because the latter cannot return angles in all four quadrants. We then convert the
atan2 output from radians to degrees and add 180 to get our angles into the range of 0-360, which is easier to work with.
Next, we determine the size of our segments in angles. For example, if we have twenty segments, each segment has a size of 18 degrees. We project our raw angle onto a single segment of
SegmentSize using the
fmod function to get the
NormalizedAngle, which tells us where along the segment this pixel exists.
We return true (one/white/visible) if the
NormalizedAngle is greater than the
GapSize, and false (zero/black/hidden) otherwise. This way, pixels that fall into gaps are hidden and all others are shown.
With the ring and the segments, we have created two separate opacity textures. By multiplying these textures together, we can produce a result that is visible only where both textures are visible (because multiplying zero by anything is zero). Et voila!
For completeness' sake, we should briefly cover how to rotate our segmented outer circle. As with most general rotations, use the
CustomRotator function. Feed a
TexCoord node into the
UVs input and feed a
Time node (slowed by some manipulations) into the
Rotation Angle input.
Use the result of the rotation function in place of the
TexCoord node as the
Coordinate input for the
Add the inner and outer circles together, which results in visibility when either circle is visible. Pass that result into the
Opacity material field.
And there you have it. The final material should look something like this:
With a few tweaks to the parameters, this is what it looks like in Rush Reborn: