Like the real world, games contain a variety of objects — each with their own appearance. In Unreal Engine, materials define these appearances. What color is it? How shiny is it? Is it transparent? These are all defined within a material.
Materials are used for pretty much any visual element in Unreal Engine. You can apply materials to various things such as meshes, particles and UI elements.
In this tutorial, you will learn how to:
- Manipulate textures to change their brightness and color
- Use material instances to quickly create variations
- Use dynamic material instances to change the color of the avatar as the player collects items
Download the starter project and unzip it. To open the project, go to the project folder and open BananaCollector.uproject.
You will see a small area containing bananas. Press Play to control a red cube using the W, A, S and D keys. You can collect bananas by moving into them.
To start, you will modify the banana material to change its brightness. Navigate to the Materials folder and double-click on M_Banana to open it in the material editor.
To adjust the brightness of the banana, you need to manipulate its texture.
At its most basic, a texture is an image, and an image is a collection of pixels. In a colored image, the color of a pixel is determined by its red (R), green (G) and blue (B) channels.
Below is an example of a 2×2 image with each pixel’s RGB values labelled.
Texture manipulation works by performing an operation on every pixel of the texture. Operations can be something as simple as adding a value to the channels.
Below is an example of clamping each channel to a range of 0.4 to 1.0. Doing this raises the minimum value of each channel which makes each color lighter.
Here’s how you would do it in the material editor:
Next, you will use the Multiply node to adjust the brightness of a texture.
The Multiply Node
The Multiply node does as its name suggests: it multiplies one input by another input.
Using multiplication, you can change a pixel’s brightness without affecting its hue or saturation. Below is an example of decreasing the brightness by half by multiplying each channel by 0.5.
By performing this operation on every pixel, you can change the brightness of the entire texture.
Although not covered in this tutorial, you can use the Multiply node in conjunction with a mask texture. Using a mask, you can specify which areas of the base texture should be darker. This is an example of masking a stone texture using a tiles texture:
Masking works because the greyscale represents the 0 (black) to 1 (white) range.
White areas have full brightness because the channels are multiplied by 1. Grey areas are darker because the channels are multiplied by values less than 1. Black areas have no brightness because the channels are multiplied by 0.
Now, it’s time to use the Multiply node.
Adjusting Texture Brightness
Break the link between the Texture Sample node and the Base Color pin. You can do this by right-clicking either pin and selecting Break Link(s). Alternatively, you can hold the Alt key and left-click on the wire.
Create a Multiply and a Constant node. You can create these quickly by holding the M key (for the Multiply node) or the 1 key (for the Constant node) and then left-clicking an empty space in the graph. Afterwards, link everything like so:
This setup will iterate over every pixel and multiply each channel by the Constant node’s value. Finally, the resulting texture is then outputted as the Base Color.
Right now, the resulting texture will be black because the multiplier is set to zero (multiplying by zero results in zero). To change the value of the multiplier, select the Constant node and go to the Details panel. Set the Value field to 5.
Click Apply and then go back to the main editor. You will see that the bananas are a lot brighter now.
Let’s spice it up by adding some different colored bananas. Although you could create a new material for each color, an easier way is to create a material instance.
About Material Instances
A material instance is a copy of a material. Any changes made in the base material are also made in the material instance.
Material instances are great because you can make changes to them without recompiling. When you clicked Apply in the material, you may have noticed a notification stating that the shaders were compiling.
This process only takes a few seconds on basic materials. However, on complex materials, the compile times can increase dramatically.
It’s a good idea to use material instances when you:
- Have a complex material and want to quickly make changes
- Want to create variations of a base material. These can be anything such as changing the color, brightness or even the texture itself.
Below is a scene using material instances to create color variations. All the instances share the same base material.
Before you create an instance, you need to create parameters in the base material. These will show up in your material instance and will allow you to adjust properties of your material.
Creating Material Parameters
Go back to the material editor and make sure you are still in the M_Banana material.
First, you need a node that will change the hue of a texture. You can use the HueShift node for this. Add one to your graph and link it like so:
[spoiler title=”Forgot how to do this?”]
- Break the connection between the Multiply node and the M_Banana node by holding the Alt key and left-clicking on the wire.
- Right click a blank space in the blueprint, search for the HueShift node, and select it.
- Connect the wires as shown in the diagram above.
Next, you need to create a Scalar Parameter node. This node holds a single value and will be editable in the material instance. You can create one quickly by holding the S key and left-clicking an empty space in the graph. Once created, connect it to the Hue Shift Percentage (S) pin on the HueShift node.
It’s also a good idea to name your parameters. Select the Scalar Parameter node and then go to the Details panel. Change the Parameter Name to HueShiftPercentage.
You can also convert Constant nodes to Scalar Parameters. Right-click the Constant node you added earlier, and then select Convert to Parameter. Afterwards, rename the parameter to Brightness.
You have now finished parameterizing the base material. Click Apply and then close the material editor.
Next, you will create a material instance.
Creating a Material Instance
Go to the Content Browser and make sure you are in the Materials folder. Right-click on M_Banana and select Create Material Instance. Rename the new asset to MI_Banana_Green.
Double-click on MI_Banana_Green to open it. This will open it in the material instance editor.
The material instance editor is composed of three panels:
- Details: This is where your parameters and other general settings will appear
- Instance Parents: Displays a list of the current instance’s parent materials. In this case, the only parent is M_Banana
- Viewport: Contains a preview mesh that will display your material instance. Rotate the camera by holding left-click and moving your mouse. Zoom by scrolling your mouse wheel.
To see the changes on the banana mesh instead, go to the Details panel and locate the Previewing section. Left-click the drop-down next to Preview Mesh and select SM_Banana. You will now see the banana mesh instead of the sphere.
Next, you will edit the parameters to adjust the banana’s color to green. To make the parameters editable, left-click the checkbox next to each parameter.
Set Brightness to 0.5 and set HueShiftPercentage to 0.2. You will end up with this:
Now that you have created your material instance, it’s time to apply it to some bananas! Close the material instance and go to the Viewport in the main editor.
Applying the Material Instance
Actors that you place into the scene can be individually edited. This means if you change the material for one banana, it won’t affect the others. You can use this behavior to change some of the bananas to green.
Select any banana and then go to the Details panel. In the component list, select the StaticMesh component.
The Details panel will update with the properties of the StaticMesh component. Change the material to MI_Banana_Green.
Repeat this process a few more times to get a better distribution of yellow and green bananas. See if you can create another material instance to make some purple bananas as well!
A Dynamically Changing Material
Materials don’t have to be entirely cosmetic; you can use them to aid in game design as well. Next, you will learn how to dynamically change the color of the cube from white to red as the player collects more bananas.
Before you create the material instance, you will need to setup the cube material.
Make sure you are in the Materials folder and then double-click on M_Cube to open it.
First, you need a way to create colors. You will see a Constant3Vector node connected to the Base Color. These nodes are perfect for picking colors because they have a red, green and blue channel.
Since the red color has already been created, you will create the white color. Add another Constant3Vector node. You can do this quickly by holding the 3 key and left-clicking an empty space in the graph.
Bring up the color picker by double-clicking on the Constant3Vector node.
Set the color to white by either using the sliders or by entering a value of 1.0 into the R, G and B channels. Afterwards, press the OK button.
To change the color from white to red, you need a way to smoothly transition between them. An easy way to do this is to use linear interpolation.
What is Linear Interpolation?
Linear interpolation is a way to find the values between A and B. For example, you can use linear interpolation to find a value that is halfway between 100 and 200.
Linear interpolation becomes even more powerful when you control the alpha. You can think of the alpha as the percentage between A and B. An alpha of 0 will return A while an alpha of 1 will return B.
For example, you can increase the alpha over time to smoothly move an object from point A to point B.
In this tutorial, you will control the alpha by using the amount of bananas collected.
Using the LinearInterpolate node
First, add a LinearInterpolate node. You can do this quickly by holding the L key and left-clicking an empty space in the graph.
Next, create a Scalar Parameter node and name it ColorAlpha. Afterwards, connect your nodes like so (notice how white is now at the top):
Summary: the LinearInterpolate node will output the value of the A input. This is because the initial value of the alpha is 0. As the alpha approaches 1, the output will approach the value of the B input.
The material is now complete. There’s more to do, but to see where you’re at so far, click Apply and then close the material editor. If you press Play, you will see that the cube is now white instead of red.
To make the cube change colors, you need to edit the ColorAlpha parameter. However, there is one problem. You cannot edit parameters on a material instance while the game is running. The solution is to use a dynamic material instance.
About Dynamic Material Instances
Unlike a regular instance, you can edit a dynamic material instance during gameplay. You can do this using either Blueprints or C++.
You can use dynamic instances in a variety of ways such as changing an object’s opacity to make it invisible. Or, you can increase an object’s specularity as it gets wet.
Another good thing about dynamic material instances is that you can individually edit them.
Below is an example of updating individual instances to mask out areas of an object.
Let’s start by creating a dynamic material instance.
Creating a Dynamic Material Instance
You can only create dynamic material instances during gameplay. You can use Blueprints (or C++) to do this.
In the Content Browser, go to the Blueprints folder and double-click on BP_Player to open it.
The first thing you will do is create a new dynamic material instance and then apply it to the cube mesh. It is a good idea to do this when Unreal spawns the actor, which is the purpose of the Event BeginPlay node.
Make sure you are in the Event Graph and then locate the Event BeginPlay node.
Now, add a Create Dynamic Material Instance (StaticMesh) node. This node will simultaneously create and apply a new dynamic material instance to the cube mesh.
Next, you need to specify which material the cube should use. Click the drop-down under Source Material and select M_Cube.
To easily reference the material later, it’s a good idea to store it in a variable. An easy way to do this is by right-clicking the Return Value pin on the Create Dynamic Material Instance node. Afterwards, select Promote to Variable.
If you look at the My Blueprint tab, you’ll notice you have a new variable. Rename it to CubeMaterial. You can quickly do this by pressing the F2 key.
Finally, link the Event BeginPlay node to the Create Dynamic Material Instance node.
Summary: once Unreal spawns BP_Player, it will create a new dynamic material instance and apply it to the StaticMesh component. It will then store the material into a variable named CubeMaterial.
The next step is to create a counter to keep track of the amount of bananas collected.
Creating the Banana Counter
If you pan down a little bit from the Event BeginPlay node, you will see the setup below. This is where you will update the banana counter and material.
The On Component Begin Overlap node will execute when the cube overlaps another actor. Next, the Cast to BP_Banana node checks if the overlapping actor is a banana. If the actor is a banana, the DestroyActor node will destroy it so it disappears from the game.
The first thing to do is to create a variable to store the amount of bananas collected. Afterwards, you will increment the variable by one every time the cube overlaps a banana.
Create a new Float variable and name it BananaCounter. Drag-click the BananaCounter variable into the Event Graph and select Get.
To increment the counter by one, add an IncrementFloat node. Once created, connect BananaCounter to it.
Next, connect the DestroyActor node to the IncrementFloat node.
Now, whenever the player collects a banana, the BananaCounter variable will increment by one.
If you were to use BananaCounter as the alpha right now, you would get unexpected results. This is because the LinearInterpolation node expects a value in the range of 0 to 1. You can use normalization to convert the counter to a range of 0 to 1.
To normalize, simply divide BananaCounter by a max value. This value is how many bananas the player needs to collect before the cube is completely red.
Add a float / float node and connect its top pin to the remaining pin of the IncrementFloat node.
Set the bottom input of the float / float node to 6. This means the cube will be completely red once the player has collected 6 bananas.
There is a small problem. When the player collects more than 6 bananas, you will get an alpha greater than 1. To fix this, use the Clamp (float) node to keep the alpha in the range of 0 to 1.
Add a Clamp (float) node and connect the Value pin to the right pin of the float / float node.
Now, that you have an alpha, it’s time to send it to the material.
Updating the Material
Drag-click the CubeMaterial variable into Event Graph and select Get.
Next, drag-click the pin of the CubeMaterial variable onto an empty space and then release left-click. This will bring up a list of nodes that can use this type of variable. Any node selected will automatically link with the variable. Add a Set Scalar Parameter Value node. This node will set a specified parameter to the supplied value.
Now, you need to specify which parameter to update. Set the Parameter Name field to ColorAlpha. This is the parameter you created in the cube material.
Link the result of the Clamp (float) node to the Value pin of the Set Scalar Parameter Value node.
Finally, link the IncrementFloat node to the Set Scalar Parameter Value node.
Here is the order of execution:
- On Component Begin Overlap (StaticMesh): Executes when the cube mesh overlaps with another actor
- Cast to BP_Banana: Checks if the overlapping actor is a banana
- DestroyActor: If the overlapping actor is a banana, destroy it so it disappears
- IncrementFloat: Increments the BananaCounter variable by one
- float / float: Divides the counter by a specified number to normalize it
- Clamp (float): Clamps the result of the division so that you don’t get a value higher than 1
- Set Scalar Parameter Value: Sets the ColorAlpha parameter of the cube material to the supplied value. In this case, the value is the normalized and clamped version of BananaCounter
It’s time to test it out! Click Compile and then close the Blueprint editor.
Click Play and start collecting bananas. The cube will start off as white and progressively become more red as you collect bananas. Once you collect 6 bananas, it will be completely red.
Where to Go From Here?
You can download the completed project here.
I love materials because they are so powerful and there are so many things you can do with them. You can layer materials together to create a complex material like stone with moss growing in the cracks. You can also make cool effects like the disintegration effect shown in the tutorial.
If you would like to learn more about materials, I recommend reading the Material Inputs page in the Unreal Engine documentation. Learning what these inputs do will allow you to create more advanced materials.
I encourage you to mess around with materials and test out a bunch of nodes (there are a lot of them). The best way to learn something is by trying it yourself :)
If you want to keep learning, check out the next post in the series, where I cover how to add UI elements like labels or buttons into your games.