I'm trying to wrap my head around how material systems like this, this are implemented. These powerful and user-friendly, graph-like systems seem to be relatively common as a method of allowing programmers and non-programmers alike to quickly create shaders. However, from my relatively limited experience with graphics programming, I'm not entirely sure how they work.
Background:
So, when I've programmed simple OpenGL rendering systems before, I typically create a Material class that loads, compiles, and links shaders from static GLSL files that I've manually created. I also usually create this class as a simple wrapper for accessing GLSL uniform variables. As a simple example, imagine that I have a basic vertex shader and fragment shader, with an extra uniform Texture2D for passing a texture. My Material class would simply load and compile those two shaders into a material, and from that point on it would expose a simple interface for reading/writing the Texture2D uniform of that shader.
To make this system a little bit more flexible, I usually write it in a way that allows me to attempt to pass uniforms of any name/type [i.e.: SetUniform_Vec4( "AmbientColor", colorVec4 ); which would set the AmbientColor uniform to a particular 4d vector called "colorVec4" if that uniform exists in the material.].
class Material
{
private:
int shaderID;
string vertShaderPath;
string fragSahderPath;
void loadShaderFiles(); //load shaders from files at internal paths.
void buildMaterial(); //link, compile, buffer with OpenGL, etc.
public:
void SetGenericUniform( string uniformName, int param );
void SetGenericUniform( string uniformName, float param );
void SetGenericUniform( string uniformName, vec4 param );
//overrides for various types, etc...
int GetUniform( string uniformName );
float GetUniform( string uniformName );
vec4 GetUniform( string uniformName );
//etc...
//ctor, dtor, etc., omitted for clarity..
}
This works but it feels like a bad system due to the fact that the client of the Material class has to access uniforms on faith alone - the user has to be somewhat aware of the uniforms that are in each material object because they're forced to pass them by their GLSL name. It's not a huge deal when it's just 1-2 people working with the system, but I can't imagine this system would scale very well at all, and before I make my next attempt at programming an OpenGL rendering system, I want to level up a bit.
Question:
That's where I am so far, so I've been trying to study how other rendering engines handle their material systems.
This node-based approach is great and it seems to be an extremely common system for creating user friendly material systems in modern engines and tools. From what I can tell they're based on a graph data structure where each node represents some shader aspect of your material and each path represents some kind of relationship between them.
From what I can tell, implementing that kind of system would be as simple a MaterialNode class with a variety of subclasses (TextureNode, FloatNode, LerpNode, etc.). Where each MaterialNode subclass would have MaterialConnections.
class MaterialConnection
{
MatNode_Out * fromNode;
MatNode_In * toNode;
}
class LerpNode : MaterialNode
{
MatNode_In x;
MatNode_In y;
MatNode_In alpha;
MatNode_Out result;
}
That's the very basic idea, but I'm a little bit uncertain about how a few aspects of this system would work:
1.) If you look at the various 'Material Expressions' (nodes) that Unreal Engine 4 uses, you'll see that they each have input and output connections of a variety of types. Some nodes output floats, some output vector2, some output vector4, etc. How can I improve the nodes and connections above so that they can support a variety of input and output types? Would subclassing MatNode_Out with MatNode_Out_Float and MatNode_Out_Vec4 (and so on) be a wise choice?
2.) Finally, how does this kind of system relate to GLSL shaders? Looking again at UE4 (and similarly for the other systems linked above), the user is required to eventually plug a some material node into a large node with various parameters that represent shader parameters (base color, metalness, gloss, emissiveness, etc.). My original assumption was that UE4 had some kind of hard coded 'master shader' with a variety of uniforms, and everything that the user does in their 'material' is simply passed to the 'master shader' when they plug their nodes into the 'master node'.
However, the UE4 documentation states:
"Each node contains a snippet of HLSL code, designated to perform a specific task. This means that as you construct a Material, you are creating HLSL code through visual scripting."
If that's true, does this system generate a real shader script? How exactly does this work?