From Material to Shaders

From cube material to the shaders on GPU

Review

Let’s focus on the Depth Prepass since this is a simple pass.

image

As we discussed, the cube drawing command has been transferred into these graphics API calls.

And we also have this image:

image

But let me ask a simple question:

âť“
where do the vertex shader and pixel shader come from?

For the depth prepass, we only need to discuss the vertex shader, which simplifies things.

The answer is that the material compiles into one or more vertex shaders and pixel shaders.

Let's discuss this further.

Material Graph

The runtime object of our material called UMaterial , but this one is not UMaterial :

image

This is UMaterialGraph. UMaterial holds a reference to it, but only in editor.

ℹ️
We don’t need this node graph when we release our game to players.

UMaterialGraph will be compiled and then transformed into actual Shader code. This portion of Shader code is what is used during runtime.

Vertex Factory

The material graph is compiled with VertexFactory to generate the final HLSL shaders.

So what exactly is VertexFactory?

In Unreal Engine, materials are independent of mesh types. If you click the UseWithXXX option, it will directly support that mesh type. We need something to abstract different mesh types, like static meshes and skeletal meshes. This is where VertexFactory comes in. You can treat it as a set of functions that is inserted into the vertex and pixel shaders, and called by those shaders to get the vertex information, without needing to know what the original mesh type was.

Compile

image

As shown in the image, the combination of the material node graph, platform settings, passes, vertex factories…, will generate multiple vertex/pixel shader pairs, instead of just one pair. And FMaterialShaderMap manages shaders and provides the appropriate ones based on rendering requests from the mesh.

And finally, the compiled shaders (which contains shader byte code) will be used to create pipeline state object. This defines all the shaders, primitive topology type and so on.

PSO will be used here :

image

Example

Let’s see an example:

This is the final vertex shader of the cube, for depth prepass:

struct FPositionOnlyVertexFactoryInput
{
	float4	Position	: ATTRIBUTE0;
	uint InstanceIdOffset : ATTRIBUTE1 ; 		
	uint DrawInstanceId : SV_InstanceID;
	uint VertexId : SV_VertexID;
};
struct FOptimizedReturn
{
	float4 OutPosition : SV_POSITION ;
};
FOptimizedReturn Main__OPTIMIZED(FPositionOnlyVertexFactoryInput Input)
{
	FOptimizedReturn OptimizedReturn;
	Main(Input, OptimizedReturn.OutPosition);
	return OptimizedReturn;
}
void Main(
	FPositionOnlyVertexFactoryInput Input,
	out float4 OutPosition : SV_POSITION
	)
{
	ResolvedView =  (ResolveView());
	float4 WorldPos = VertexFactoryGetWorldPosition(Input);
	{
		OutPosition =  MakePrecise( mul(WorldPos, ResolvedView.TranslatedWorldToClip) );
	}
}

Basically this shader did two things:

  • Get world position from the input, with LocaVertexFactory ’s VertexFactoryGetWorldPosition
  • Translate the world space position into clip space position and return.

Usage of the Vertex Factory

You may want to ask, why we need VertexFactory here? Can we directly get the position from Input?

Yes, for the cube, you can do so. But I can give you some reasons:

  • If there are more instances, we need LocaVertexFactory to do instancing, translate the vertex based on each instance’s transform.
  • We may need other vertex info for complex vertex node graph in our material.
  • We may use the material for skinned meshes. We need the factory to do vertex skinning for us.

Summary

Finally, let’s have a summary

image

Questions for the Next Chapter

Now we have identified one of the sources of the data: Shader.

This is not the end, there is still a significant portion of data whose source we have yet to determine, and that is the vertex data used for rendering. How are they read and utilized?