Today I worked out how to make a new node in the material editor of the Unreal 4 Engine. This is a quick introduction on how to do it, I’ll hopefully have more detailed tutorial once I work out more details on how the system works.
As part of my work to get the rendering system I want working in the Unreal engine I’ve been diving around in the renderer, shader and material code of the engine. Today I managed to make a new node appear in the material editor! I haven’t tested this out fully yet but I thought I’d detail what I did in case anyone else is trying to do the same. You’ll need to grab the source off github if you haven’t already to follow though.
Material Compilation
Material nodes in the editor map more or less directly to HLSL functions that are stored in various files of the Shaders sub directory. When a material is compiled it’s turned into HLSL code using the ‘MaterialTemplate.usf’ shader file. *.usf is just an Unreal extension for HLSL shaders, the unreal documentation on shaders is pretty lacking so there may be some differences but I haven’t found any yet. When you apply your changes to a material the FHLSLMaterialTranslator class creates code to fill in functions in this file, you can view this code by going Window->HLSL Code.
Creating a New Node
First open up the Engine\Source\Runtime\Engine\Public\MaterialCompiler.h file, this contains functions for a lot of the basic nodes used in the material editor. If you go back to the Shaders folder and open ‘Common.usf’ you’ll see a lot of these functions map to HLSL functions in this file. The FHLSLMaterialTranslator class is actually a subclass of the MaterialCompiler. As far as I can tell translator is actually a better term since this class really generates code not compiles it.
The other place you’ll need to look is ‘Engine\Source\Runtime\Engine\Classes\Materials\’, in here you’ll see a bunch of files called MaterialExpression*.h. These each contain a UCLASS for the actual nodes that appear in the editor. The implementations are all in \Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressions.cpp.
To make a new node I did the following:
- Created a new HLSL function in Common.usf.
- Created a corresponding function in MaterialCompiler.h in both the virtual base class as well as the proxy class below it.
- Created an implementation for the new function in Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.h.
- Created a new MaterialExpression*.h file in the materials folder and filled in it’s implementation in MaterialExpressions.cpp.
And that’s all there is to it! I’m not including any code as an example since it all involves modifying engine code. But if you look in all of the files I mentioned you should be able to see the pattern. You can see the result below:
Why Bother?
That all probably seems like a lot of work just to get a new node. You can already do this by creating a material function so why even bother? Well some code is much simpler written in actual code than a material. For example here’s a sobel operator in a material:
In actual code that’s only about 10 – 15 lines. The actual code is much easier to reason about, when doing so much wiring of nodes it’s hard to keep track of what’s what and easy to make a mistake. I’d like to be able to integrate HLSL code into materials more easily. There is a custom node function in which you can write HLSL code there’s no easy way to use it in multiple materials unless you wrap it in a material function.
The disadvantage of doing it this way is that you have to modify the engine, recompile it (only a couple modules need to compile so it’s actually pretty quick) and then wait for all the shaders to recompile (this is the slow part). It would be hard to have quick iteration using this system. This may also interfere with the optimisation that the material compiler performs but I haven’t done any profiling yet.
What Next?
I only created a basic test node today but I want to experiment with making more complex input and output nodes. In the long run I want to be able to write to a custom GBuffer for use in the post processing stage of the rendering.
I mentioned that iteration is pretty slow with this. If you modify Common.usf all shaders that use it need to re-compile, I’m going to experiment with seeing if I can place custom functions in another file to prevent this.
I’d also like to try and make it more modular. Having to edit so many engine files each time is kinda inconvenient. Since material expressions are a UCLASS is should be possible to make the expression class in a plugin or game module. I’m hoping I can put a base function in the compiler/translator that will call out to classes in other modules for compilation. It’s probably not possible to completely isolate the changes to a plugin but it can probably be minimised.