Hi, all. Today's post is going to focus on the problem solving and methods I used to create a 3D menu solution. Overall, I think the result turned out as I imagined, and this project encompasses knowledge of a lot of aspects of the engine, from UI and Unreal's slate system, the Material Layering System, and Blueprint's and C++. My main focus in this post is to discuss the shader and material techniques, but it will cover some of the challenges solved in the creation of my solution, in case anybody else needs a similar result.
Concept draft for what the final product would look like.
An example of the common approach. In some use cases, this would be great. But my system needed to be a bit more robust.
The Issue
If one were to go search for tutorials or solutions to make a menu with depictions of 3D assets, the main solution people will be pointed to is this :
Create a render target
Create a scene capture component
Render an object in the scene capture to the render target
Set a UI element in your menu to use the render target as a texture
There's a couple of issues with this approach, which is why I set out to make my own solution.
What if I want to have upwards of hundreds of 3D objects in my menu?
This would entail the creation of several render targets, and scene captures. This is not only inefficient from a creation standpoint, it is also inefficient from a performance standpoint because we are rendering the scene several times for something which should only really be rendered once.
Render Targets are texture based.
Since the solution would rely on the rendering of several render targets, it would mean that resolution and memory footprints start to become an issue. It would take the right amount of experimenting to find the best resolution to capture detail, without using too much memory for your menu.
The Solution
So with lots of trial and error, I eventually landed at a solution which worked for me:
Create the menu within the 3D scene using shader effects, meshes, and 3D widget components.
But while creating this menu solution, there were a number of complications which made this a more difficult solution to implement than the traditional render target method.
By using a 3D Widget component, you are now working in world space, instead of screen space. Thus keeping track of things, for instance, your inventory slots, becomes a lot more complicated since you need to transform your widget from where it is on the screen, into where it is in the game world.
By displaying 3D meshes on top of your menu, you need a way to standardize their behavior. For example, making the item rotate or disappear if you change a menu. Doing this all through CPU tasks might make the menu more sluggish than it needs to be, and there might be some ways to solve these issues by pushing them to the GPU.
I am going to discuss the ways I solved these challenges, and how they relate to my main passion : Shaders and Materials.
A 3D Widget Component?
First and foremost, in order to have a 3D menu work with actual game objects within the scene, you need a way to represent and place this menu within the world.
Unreal provides a solution to this called a 3D Widget component, which allows you to place widgets on a plane to be rendered out into the world. One caveat though is that now that your widget is in 3D space, it is subject to Z-Order complications, and it might render over components that you don't want it to.
An easy fix to this is to edit the default Widget3DPassThrough material. In my case it was to edit it so that objects rendered in the stencil buffer make the material transparent, so that way objects seem to always 'render on top'.
By editing the default passthrough material, I made it so that objects rendered in the stencil buffer can effectively "cut through" or seem like it renders over the 3D widget.
Finding Where Our Widget Component Is
After we have the ability to represent our menu in 3D space. We need to be able to find where individual components, and get their positions. This is accomplished in two steps.
First I create a function which gets the absolute position of the widget and save that as a variable. In my case, I wanted to keep track of inventory slots in a grid, so every single grid slot will use this function to get their position, and update it every tick.
Secondly, in the blueprint which contains my widget, I calculate and transform this position into world space using the absolute position, and the inverse of the widget components scale, since absolute position goes from top-down. This is then multiplied by the right and up vector of the component and added to the position of the top left corner of the component.
These calculations are iterated through every single thing I want to keep track of in regards to their position. So arrays and for loops are your friend. Additionally, this could be further optimized by turning into C++, but for prototyping purposes, blueprints made this easier for quick iteration and testing.
You can then use these positions to snap various 3D static meshes to the positions of each component. In my solution, I went with a wrapbox carrying several inventory slots, all wrapped inside a scrollbox. One thing to note was that I needed to calculate the absolute positions in relation to an offset that is provided by the scrollbox widget. But this is as simple as adding the offset variable within my scrollbox, to my calculation for each slot's absolute position.
Standardizing Behavior Using Shaders and The Material Laying System
One thing that I have been really experimenting with lately is the usage of Unreal's Material Layering system. In short, the Material Layering System behaves like a material function which allows you to create modular components to set material attributes, and blend these attributes in specific ways.
The way I use it is to standardize behavior for things like masking object to the extents of the menu or having different material attributes, but within the same shader.
Below are two examples of how I use material layers to my advantage to create a more modular shader workflow.
Example 1. Cube Mask to match the visual of the menu
One thing that I used material layers for was to mask out the meshes to align with the scrollbox edges of my menu. This entailed making a custom material function, and using that function to override opacity within my materials.
Notice how the top part of the grid where objects are getting cut off. This is accomplished through a material layer.
A material function made to output whether a position in within the extent of a cube. A cube mask, so to speak.
Using the above function in a material layer. This is effectively a module which overwrites the opacity of a material based off of the cube mask.
Example 2. Overwriting Attributes using a UV Atlas System
Another thing I used material layers for was to effectively mask out different materials based off of custom UVs. This is a method I adapted from the developer PrismaticaDev, in an attempt to reduce draw calls. It entails having a second UV channel dedicated to masking out different materials with different properties. In my below example, I use this to mask the main body of the vending machine, and the panel and food items, each as separate "materials".
My blending logic. It divides a UV channel into a specified amount of columns. If a vertex is somewhere in a specific column, it will get the properties of that material. Refer to the images on the right.
Note all the overlapping UVs. Normally this would be bad. But using our masking technique, we essentially get 2 material slots with 1 less draw call.
Without the layer masking, the overlapping UVs are a problem.
With the material layer masking logic applied, we have 2 materials for the price of one draw call!
The second UV channel baked with atlas information. On the right are UVs for the food and panel. The left is the window and main body.
Shader Tricks Used For Visuals
Below are a couple of shader tricks I used for some visual flair. Some of these tricks are great because they help optimize the menu, and push some of the calculation of the visuals towards the GPU.
Here is some logic I use to rotate an object using World Position Offset. This is what I use for the objects in the inventory grid to rotate.
Using this method is actually beneficial because it stops the rotating logic from needing to be run on the CPU through something like 'Set Rotation'.
Here, I use a gradient texture to animate a "paint brush drag" in a circle pattern. You can see this when you hover over an object.
The gradient texture is packed with 3 variations, each being packed into a color channel. This makes the paint stroke feel more varies, and allow me to lerp for hundreds of slightly different brush strokes.
Here, I use an Signed distance Field texture as a way to animate a 'paint drip' shape. All I have to do is create an SDF texture with multiple shapes linked to color channels, and lerp through. By using a smooth step, I am able to change the shape and how thick the shape is, or potentially add things like outlines to the shape.
In the upper right corner of the square, you can see the animation in action. All of this is accomplished using a 512x512 RGB8 texture. No flipbooks needed.