Location Based Saturation in Unreal Engine 4

During the Epic MegaJam 2017 I made one implementation of a Location Based Saturation that over time transitions from being desaturated to fully saturated. This is the technical implementation of that feature. The effect is demonstrated in this clip.

Abstract

This implementation is for N amounts of locations in the game that will have a sphere of influence if something is desaturated or saturated. The implementation uses 7 material nodes excluding the color input for the desaturation node. It uses 4 shader instructions and 15 instructions per location. For 1 location it would be 19 instructions, for 2 it would be 34 and so on. In the game mode's begin play event it sets the TotalCharacters scalar and tells the characters which Vector parameter they are affecting. The technique is a modification of a tutorial that Alan Willard at Epic Games made.

The material code

The material code for the master material looks like in the image:

The logic is pretty straight forward.

  • Get largest sphere mask value for each location which in this implementation comes from the huggable character.
  • Add global saturation value and clamp it between 0 and 1. If you don't clamp the value it can go negative which inverts the colour.
  • 1 - clampedValue will give the desaturation fraction. Which for 1 = desaturated, 0 = saturated

The HLSL code

The finished HLSL code for the node GetSaturation used in the game:

            
// This returns the largest sphere influence found, if 1 it's it will be fully colored
// If 0 meaning no sphere has influenced the point.
float result = 0; 

// Get scalars[0] which is the x value. 
int totalCharacters = (int)MaterialCollection0.Vectors[0].x;
 
for (int i = 0; i < totalCharacters; i++)
{        
    // Because Vectors[0] is occupied by the scalar values we need to do i+1 to start at Vectors[1]
    // xyz = the world position
 // w = radius of sphere
    float4 location = MaterialCollection0.Vectors[i + 1];

 // Sphere Mask by copying HLSL code from generated HLSL code from material editor  
    float3 distance = (location.rgb - worldPosition); // A - B   
    float distanceDot = dot(distance, distance); 
    float squaredDistanceDot = sqrt(distanceDot);
    float Local4 = (squaredDistanceDot * location.a); 
    float Local5 = (1.00000000 - Local4);
    float Local6 = (Local5 * 6.66666508); // 1.0f / FMath::Max(1.0f - HardnessPercent * 0.01f, 0.00001f);  HardnessPercent = 85
    float sphereMaskValue = min(max(Local6, 0.00000000), 1.00000000); // clamp

    result = max(result, sphereMaskValue);
}
 
return result;
                
            
There are some things I learned trying to write this HLSL code.

Sphere mask function

Firstly there is no SphereMask function in HLSL as there is when using the material editor node SphereMask. Instead I got help from a user called Deathrey on the Unreal Slackers discord in finding out where to look. It was in the unreal engine source file Engine/Source/Runtime/Engine/Private/Materials/MaterialExpressions.cpp. With the implementation and the generated HLSL code from the material I understood slowly what was what and how to reimplement the sphere mask functionality. This is the HLSL implementation of a sphere mask node. I tried renaming the variables as UE compiler just names them Local1, Local2, ...

                 
// A - B  
float3 distance = (A.rgb - B.rgb); 
                    
float distanceDot = dot(distance, distance); 
float length = sqrt(distanceDot);
float scaledLength = (length * radius); 
float Local5 = (1.00000000 - scaledLength);

// The value 6.66...  is precomputed by UE shader compiler by the following c++ logic:
// HardnessPercent = 85.0f
// 1.0 / FMath::Max(1.0f - HardnessPercent * 0.01f, 0.00001f);  
float Local6 = (Local5 * 6.66666508); 
float sphereMaskValue = min(max(Local6, 0.00000000), 1.00000000); // clamp
                
            

MaterialCollection0

Initially I tried to access the scalars & vectors this way. There's not a lot of documentation on Material Parameter Collection usage in material expression custom nodes that I could find.

                
MaterialCollection0.Scalars[0]
MaterialCollection0.Vectors[0]
                
            
I used this unreal engine forum post to learn how to use the Vectors. It didn't mention how to use the Scalars. Which I learned by digging through the unreal engine source code! The scalars occupy the first (N / 4) amount of array entries for the Vectors so for example 3 scalars it occupies Vectors[0] and for 5 scalars it would be [0] and [1].
One final important thing that the user in the forum post wrote is this:
Actually they can only be read if at least one Parameter is set as an input for your custom expression.
Which in the implementation is automatically solved through the Global Saturation scalar parameter.

The game mode blueprint code

The totalCharacters was set by getting the total amount of characters and store it in an int. It was also used to check for win condition!

The material parameter collection vector slots were set on begin play by iterating over all characters and use their index in the array.

When a character was hugged

When a character was hugged all that's needed to do is update the Vector slot in the material parameter collection. Which was done on tick while the character was hugged by increasing the sphere of influence's radius. The saturation sphere variable is a Sphere Collision.

Conclusion

I'm happy with the result and it looks great ingame.
I was worried about performance initially when going up to 8+ characters since it's every material in the whole game doing this but on my pc it ran fine.
There's probably a better way of doing the location based saturation but this is what I came up with during the Epic MegaJam 2017.

No comments:

Post a Comment