Allie Keats
Game Programmer
Senior Systems Programmer at Behaviour Interactive with 5 years of professional C++ experience.

Capstone - Semester 2 Week 5

By: Allie Keats | 16 February 2018

Could it really be blog post time again already?

HUD Overhaul

First, let’s take a look at the new HUD:

For the most part this included few code-related changes. We’re still using the “BoundProperty” system which uses C# events to auto-push updates to UI elements without the “user code” having to know how that data is being used and displayed. The durability indicators, which used to exist in the ammo panel, have been removed and will be replaced in favor of a durability display directly on the model, coming next sprint. Luckily, the same system that used to display the durability has been repurposed to show the “ammo type” whenever the mechanism changes. A similar system was used to change the center reticle whenever the barrel changes.

The other change included writing a new BoundPropertyListener for the draining bars. This binding element takes a “current value” property, a “max value” property or an inspector-set max value, then calculates a fill percentage based on this whenever either item changes. Note: in the video above, these bars drain vertically, but we decided after it was recorded to have them drain outwards horizontally instead for clearer visual distinction.

First-Person Viewmodel

In the above video, you might’ve noticed a new robot hand holding the weapon. One of my tasks this sprint also involved helping our lead artist, Tyler, implement the first pass of the hand into the game. For the most part, that was simply a matter of changing around some parenting in the hierarchy and ensuring the correct items get enabled/disabled depending on if a player is the local player or a remote/networked player.

There was one catch for implementing this hand, though. Going forward, we are planning on having team play with team colors. One option was to have separate texture sets for each team, but the preferred way was to do it in a more programmatic fashion so that we could easily change things later on. Tyler came up with a clever solution for this; in addition to the standard PBR maps, he also provided me with a one-channel “color mask” map. Wherever this mask was white, our Material-defined team/player color would be multiplied onto the diffuse. Wherever it was black, the color would be ignored. This obviously required a custom shader. I’m not a graphics person, but thankfully Unity made this easy, as it was just a surface shader.

My first attempt at this was the following surface shader:

CGPROGRAM
#include "UnityPBSLighting.cginc"
#pragma surface surf Standard

sampler2D _MainTex;
sampler2D _ColorMask;
fixed3 _ColorMaskColor;

void surf(Input IN, inout SurfaceOutputStandard o)
{
       fixed4 albedo = tex2D(_MainTex, IN.uv_MainTex);
       fixed3 mask = tex2D(_ColorMask, IN.uv_MainTex).rgb;
       // Grab the other PBR textures here...

       if (length(mask) > 0.5)
              albedo.rgb *= _ColorMaskColor;

       o.Albedo = albedo.rgb;
       // Fill the rest of the PBR output struct here
}

ENDCG

It essentially determines the value of the color mask and then conditionally multiplies based on that. However, if I’ve learned anything in my graphics class, it’s that conditional branching in shaders is a death knell for the GPU. So, after a bit of thought and discussion with some other programmers, we were able to get it down to the following:

CGPROGRAM
#include "UnityPBSLighting.cginc"
#pragma surface surf Standard

sampler2D _MainTex;
sampler2D _ColorMask;
fixed3 _ColorMaskColor;

void surf(Input IN, inout SurfaceOutputStandard o)
{
       fixed4 albedo = tex2D(_MainTex, IN.uv_MainTex);
       fixed3 mask = tex2D(_ColorMask, IN.uv_MainTex).rgb;
       // Grab the other PBR textures here...

       // Invert the mask and add the color, then clamp.
       // Result is what used to be black is now white,
       // and what used to be white is now the overlay color.
       mask = fixed3(1, 1, 1) - mask + _ColorMaskColor;
       mask = clamp(mask, fixed3(0, 0, 0), fixed3(1, 1, 1));

       // ... which means we can just multiply!
       albedo.rgb = albedo.rgb * mask;

       o.Albedo = albedo.rgb;
       // Fill the rest of the PBR output struct here
}
ENDCG

It’s a small change and a minor improvement, but profiling through Unity revealed that it saves a conditional branch (obviously) and about 10 operations on the GPU total. I consider that a great optimization considering how easy it was to do! All-in-all, this was a successful sprint (including the weapon view refactoring from my last blog post) and I’m looking forward to sharing what I’m working on this sprint next Friday!

Back to blog