Renaud van Strydonck

Renaud van Strydonck

Portrait of a Game [ Designer / Developer ]

Vertices, Pixels and Shaders(Oh, My!)

“Just the place for a Snark!” the Bellman cried,
   As he landed his crew with care;
Supporting each man on the top of the tide
   By a finger entwined in his hair.
“Just the place for a Snark! I have said it twice:
   That alone should encourage the crew.
Just the place for a Snark! I have said it thrice:
   What I tell you three times is true.”

So, ironically, the move from Programmer to Game Designer has afforded me the opportunity to write an awful lot more Shaders… I know, it doesn’t make any sense to me either… Just to be clear, it hasn’t afforded me any more time to write Shaders (quite the contrary, in fact) but I’m grateful none the less, because Shaders are delicious (there, I said it).

Shaders are basically like Photoshop actions (don’t look at me like that, it was that or Instagram filters) : They take a variety of inputs (diffuse textures, normal maps, cube maps, colours, floats etc.) and combine them in a sequence of calculations to achieve a particular visual effect.
They’re no substitute for artistic talent and a strong visual target (and will probably cause you problems if you use them without understanding what they do) but they’re an incredible tool in the right hands.  In some cases, entire game mechanics have evolved around a well-designed Shader – and art that supports the game that supports the art etc. is one of the tastiest of recursions!*

Shaders exist in a complicated space, though : they’re often tricky to program, difficult to debug, and poorly documented.  They require the programmer to understand the visual effect they’re trying to achieve BUT they’re generally more exciting to artists than they are to programmers and usually require an artist to properly apply them anyway… but if you are a programmer : program Shaders for your artists.

Do it on the side, while other bits and pieces are on hold, because there is nothing more beautiful than the smile of an artist who has just been given a new weapon.  Part-lust, part-wonder, part-childlike joy… and they will craft such wonders beyond your wildest imaginings (well, technically you should probably know exactly what your Shader is capable of, actually, but you get the idea).

Aaanyway, wandering around the studio while uScript completed it’s unreasonably long compilation dance, I spotted a gaggle of artists gathering around what looked suspiciously like code.

DawnOfMan

They were looking for a way to blend two tileable textures together using the vertex colour of their environment mesh, and had gathered together a few examples which our core programmer was staring at grimly while they pointed and chattered explanations.

As I might have mentioned, Shaders are delicious.

I sensed weakness.

I pounced.

Here is the product of that first ecstatic hunt.  I’ve commented it as best I could, and I hope that you’ll find it reasonably useful.  If you have any questions, comments or suggestions, PLEASE feel free to contact me.  Shaders are wild mysterious creatures and I have an awful lot to learn.

A Simple Surface Shader Blending Two Textures by Vertex Colour :

// This shader will blend two textures using the vertex color as a guide.

Shader "FX/Diffuse Blend"
{
// These are the fields that will appear for this Material in the Inspector - see Unity doc at http://docs.unity3d.com/Documentation/Components/SL-Properties.html
// The first part is the property's variable, eg. "_MainTex" - Naming your main texture "_MainTex" signals to Unity that this Shader should be included in Lightmap calculations
// Using the standard Unity property names also lets Unity apply a fallback Shader if the user's hardware can't handle this one
// The second part in brackets is the name of the property in the Inspector, eg. "Main Texture" - This can be anything you like.
// The third part in brackets is the variable type, eg. "2D" declares it to be a texture file -
// The fourth part is the property's default value, eg. "white {}" - Textures are generally given default values of white, gray or black
Properties
{
_MainTex ("Main Texture (vertex A = black)", 2D) = "gray" {} // This is the material's Texture property and we want it to be included in Lightmap calculations (especially LightProbes)
_BlendTex ("Blend Texture (vertex A = white)", 2D) = "gray" {} // This shader is a little strange since we actually want to blend two textures, this is the texture we will blend in
_ColorTint ("Main Color", Color) = (0.7, 0.7, 0.7, 0.1) // This is a color value we will use to tint the texture after it is blended, but before the light is applied
}

// The SubShader is where the Shader calculations actually happen
SubShader
{
// This allows Unity to intelligently substitute this Shader when needed
Tags { "RenderType"="Opaque" }
Blend Off
LOD 250

// The syntax that was use after "pragma" tells Unity what kind of shader this is and how it should be applied, eg.
// This shader is a surface Shader of type Lambert, and don't want Unity to bother with a prepass because we're not doing anything with light
CGPROGRAM
#pragma surface surf Lambert exclude_path:prepass

//These match the shader properties
uniform sampler2D _MainTex;
uniform sampler2D _BlendTex;
uniform fixed4 _ColorTint;

// This contains the inputs to the surface function
// Valid fields are listed at: http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaders.html
// The color syntax is actually accessing the vertex color that we put in our object to guide the blend. You normally don't need this
struct Input
{
float2 uv_MainTex;
float2 uv_BlendTex;
float4 color : COLOR;
};

// This is where we prepare the surface for lighting by propagating a SurfaceOutput structure, I'm going to break it down so you know what's happening
// In the first two lines, we're unwrapping both textures
// "Lerp" is a word programmers use when they're blending between two sets of numbers, this is what we're doing with our textures
// So we're Lerping between our main texture and our blend texture, and we're using the values of the vertex color to guide us... in theory this could be any value set
void surf (Input IN, inout SurfaceOutput o)
{
fixed4 Tex2D0 = tex2D(_MainTex, IN.uv_MainTex);
fixed4 Tex2D1 = tex2D(_BlendTex, IN.uv_BlendTex);
fixed4 Lerp0 = lerp(Tex2D0, Tex2D1, IN.color);

o.Albedo = Lerp0.rgb * _ColorTint; // We modulate our colours by our main colour, BUT we also multiply it by our tint... this works a little like a color filtre in Photoshop
o.Alpha = 1.0; // No alpha in this shader
}
ENDCG

}
FallBack "Mobile/Diffuse" // Shader to use if the user's hardware cannot incorporate this one
}

Blended Diffuse Shader Example

Our prodigious environmental artists, Alexandre Lutz and Daljit Singh, were kind enough to provide me with a vertex painted mesh and a couple of gorgeous textures to show you what these Shaders can do (in Alex’s defence, he originally gave me an exquisite tree-house model to use, but it turned out to be rather too intricate to get the point across).  Check out their portfolios to see what you could have won.

I was young, I was foolish, I had no idea what pent-up hunger that simple little Shader would unleash. How many dreams had been set aside for lack of the right Shader, and would now be dragged kicking and screaming back out into the thin light of day.

They had waited long enough, and Shaders are delicious.

They sensed weakness.

They pounced.

My second request was a slightly more complex version of the first one : A shader that would blend between two textures, guided both by the vertex colour and the alpha layer of the main texture, so that e.g. the sand texture would appear in the cracks of the rock texture, or the water texture would appear to flow in between the gaps of the brick texture.

A Simple Surface Shader Blending Two Textures by Vertex Colour and Alpha Channel:

// This shader will blend two textures using the vertex color AND the alpha of the main texture as a guide
// This lets us have a more realistic blend distribution, such as water filtering in between the gaps in brickwork

Shader "FX/Diffuse Alpha Blend"
{
 // These are the fields that will appear for this Material in the Inspector - see Unity doc at http://docs.unity3d.com/Documentation/Components/SL-Properties.html
 // The first part is the property's variable, eg. "_MainTex" - Naming your main texture "_MainTex" signals to Unity that this Shader should be included in Lightmap calculations
 // Using the standard Unity property names also lets Unity apply a fallback Shader if the user's hardware can't handle this one
 // The second part in brackets is the name of the property in the Inspector, eg. "Main Texture" - This can be anything you like.
 // The third part in brackets is the variable type, eg. "2D" declares it to be a texture file -
 // The fourth part is the property's default value, eg. "white {}" - Textures are generally given default values of white, gray or black
 Properties
 {
 _MainTex ("Main Texture (vertex A = white)", 2D) = "gray" {} // This is the material's Texture property, we'll also be using it's alpha to guide our blend texture
 _BlendTex ("Blend Texture (vertex A = black)", 2D) = "gray" {} // This shader is a little strange since we actually want to blend two textures, this is the texture we will blend in
 _ColorTint ("Main Color", Color) = (0.7, 0.7, 0.7, 0.1) // This is a color value we will use to tint the texture after it is blended, but before the light is applied
 }

 // The SubShader is where the Shader calculations actually happen
 SubShader
 {
 // This allows Unity to intelligently substitute this Shader when needed
 Tags { "RenderType"="Opaque" }
 Blend Off
 LOD 250

 // The syntax that was use after "pragma" tells Unity what kind of shader this is and how it should be applied, eg.
 // This shader is a surface Shader of type Lambert, and don't want Unity to bother with a prepass because we're not doing anything with light
 CGPROGRAM
 #pragma surface surf Lambert exclude_path:prepass

 //These match the shader properties
 uniform sampler2D _MainTex;
 uniform sampler2D _BlendTex;
 uniform fixed4 _ColorTint;

 // This contains the inputs to the surface function
 // Valid fields are listed at: http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaders.html
 // The color syntax is actually accessing the vertex color that we put in our object to guide the blend. You normally don't need this
 struct Input
 {
 float2 uv_MainTex;
 float2 uv_BlendTex;
 float4 color : COLOR;
 };

 // This is where we prepare the surface for lighting by propagating a SurfaceOutput structure, I'm going to breaqk it down so you know what's happening
 // In the first two lines, we're simply unwrapping both textures
 // Blend0 is the value we'll use to interpolate between the two :
 // We're taking the biggest of the-alpha-plus-the-vertex-color and the-vector-color, multiplying it by the vertex color, and clamping it between 1 and 0
 // "Lerp" is a word programmers use when they're blending between two sets of numbers, this is what we're doing with our textures
 // So we're Lerping between our main texture and our blend texture, and we're using the values of the vertex color to guide us... in theory this could be any value set
 void surf (Input IN, inout SurfaceOutput o)
 {
 fixed4 Tex2D0 = tex2D(_MainTex, IN.uv_MainTex);
 fixed4 Tex2D1 = tex2D(_BlendTex, IN.uv_BlendTex);
 fixed4 Blend0 = saturate(max((Tex2D0.a + IN.color), IN.color) * IN.color); // "saturate" is a syntax that actually just clamps a value between 0 and 1, "max" takes the biggest of two values
 fixed4 Lerp0 = lerp(Tex2D0, Tex2D1, Blend0);

 o.Albedo = Lerp0.rgb * _ColorTint; // We modulate our colors by our main color, BUT we also multiply it by our tint... this works a little like a color filtre in Photoshop
 o.Alpha = 1.0; // No alpha in this shader
 }
 ENDCG

 }
 FallBack "Mobile/Diffuse" // Shader to use if the user's hardware cannot incorporate this one
}

Alpha Blended Diffuse Example

We’re zooming into Alex’s magnificent plane a little, here, to get a good look at the difference between the two.  If we had a larger blend area between the vertex colours, we would probably see the effects from further away.  The real joy, here, is in getting sand between your bricks (if you know what I mean).

This Shader is a little trickier to handle, since you actually get the best effect by inverting your Alpha (which really means that I need to take another look at the code), but it gets you some really lovely effects in the hands of a real artist.

And so our project suddenly got more intricate and beautiful.  Our hungry artists started using these simple Shader in weird and unexpected ways, showing me in their own way that although they hadn’t written them, they understood these Shaders far better than I ever could…

And I loved them for it.

Which is probably why, when I next saw their grinning faces peer over the top of my monitor, I did the unthinkable… I smiled back.

We never ended up using this one, because it doesn’t run properly on mobile devices… which is a crying shame, because it’s really rather clever.  If any of you out there in the great internet ocean know what the problem might be, I’d love your insight.

A Not-so-Simple Surface UV-Offset Shader :

Shader "FX/Additive Flow"
{
 Properties
 {
 _MainTex ("Base", 2D) = "white" {}
 _XMul ("UV.x mul", float) = 0.25
 _YMul ("UV.y mul", float) = 0.25
 _Speed ("Speed", float) = 0.05
 _Tint ("Tint", Color) = (1.0, 1.0, 1.0, 1.0)
 }

 SubShader
 {
 Tags
 {
 "RenderType"="Transparent"
 "Reflection" = "RenderReflectionTransparentAdd"
 "IgnoreProjector"="True"
 "Queue"="Transparent"
 }

 Blend SrcAlpha One
 Cull Off
 Lighting Off
 ZWrite Off
 Fog { Mode Off }
 LOD 200

 CGPROGRAM
 #pragma surface surf NoLighting
 #pragma 3.0

 uniform sampler2D _MainTex;
 uniform fixed4 _Tint;
 uniform fixed _XMul;
 uniform fixed _YMul;
 uniform fixed _Speed;

struct Input
 {
 float2 uv_MainTex;
 INTERNAL_DATA
 };

void surf (Input IN, inout SurfaceOutput o)
 {
 fixed phase = _Time[1];

 fixed2 flowDir = fixed2(_XMul, _YMul) * _Speed;

 fixed2 flowUV = IN.uv_MainTex + flowDir * phase;

 fixed4 tex0 = tex2D(_MainTex, flowUV);

 o.Albedo = tex0.rgb * _Tint;

 o.Alpha = 1.0;
 }

 fixed4 LightingNoLighting(SurfaceOutput s, fixed3 lightDir, fixed atten)
 {
 // Declare the variable that will store the final pixel color,
 fixed4 c;
 // Copy the diffuse color component from the SurfaceOutput to the final pixel.
 c.rgb = s.Albedo;
 // Copy the alpha component from the SurfaceOutput to the final pixel.
 c.a = s.Alpha;
 return c;
 }
 ENDCG
 }
 FallBack "Mobile/Particle/Additive"
}

Since Production hit us pretty hard after that, I concentrated on Game Design for a while, occasionally moonlighting for the Art Department to make various Shaders (both mine and others’) double sided for the flags/robes/waterfalls etc. that players might see from various angles.  Small-fry, interesting from the point of view that it gave me excuses to do more Shader research, but hardly ground-breaking stuff… I tried to talk the artists into some Non-photo-realistic Shaders (my particular poison of choice) but they wisely declined.

Finally, while I was hovering behind out Character Artist’s shoulder, he mentioned how cool it would be if the models could look like they do in ZBrush… I thought they probably could.  It turned out he had been doing some reading into MatCap shaders and, his trap sprung, quickly pulled up a few links and talked me through his idea.

The fruit of our conversation was a MatCap shader that would show off his hard-earned Bumpmaps without requiring any Ddynamic Lighting.  After a little work, I squeezed Lightprobe support into the beast, and we used it for every single one of our characters from then on.

An Actually-Not-That-Complicated Bumped MatCap Shader Supporting Lightprobes :

Shader "FX/MatcapBumpedLP"
{
 Properties
 {
 _MainTex ("Base (RGB)", 2D) = "white" {}
 _BumpMap ("Bumpmap (RGB)", 2D) = "bump" {}
 _MatCap ("MatCap (RGB)", 2D) = "white" {}
 _LPScale ("Light Probe Influence", float) = 1
 }

Subshader
 {
 Tags { "RenderType"="Opaque" "BW"="TrueProbes"}
 Fog { Mode Off }

Pass
 {
 Name "BASE"
 Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
 #pragma vertex vert
 #pragma fragment frag
 #pragma fragmentoption ARB_precision_hint_fastest
 #pragma multi_compile LIGHTMAP_ON LIGHTMAP_OFF
 #include "UnityCG.cginc"

uniform sampler2D _MatCap;

 uniform sampler2D _MainTex;
 uniform fixed4 _MainTex_ST;

 uniform sampler2D _BumpMap;
 uniform fixed4 _BumpMap_ST;

 uniform fixed _LPScale;

struct v2f
 {
 fixed4 pos : SV_POSITION;
 fixed2 uv : TEXCOORD0;
 fixed2 uv_bump : TEXCOORD1;

 fixed3 SHLighting : TEXCOORD3;

 fixed3 TtoV0 : TEXCOORD4;
 fixed3 TtoV1 : TEXCOORD5;
 };

v2f vert (appdata_full v)
 {
 v2f o;

 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
 o.uv_bump = TRANSFORM_TEX(v.texcoord, _BumpMap);

 fixed3 worldNormal = mul((fixed3x3)_Object2World, SCALED_NORMAL);
 fixed3 shl = ShadeSH9(fixed4(worldNormal, 1.0));
 o.SHLighting = shl * _LPScale;

 TANGENT_SPACE_ROTATION;
 o.TtoV0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
 o.TtoV1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);

return o;
 }

float4 frag (v2f i) : COLOR
 {
 fixed4 tex = tex2D(_MainTex, i.uv.xy);

 fixed3 normals = UnpackNormal(tex2D(_BumpMap, i.uv_bump));

fixed2 capCoord = half2(dot(i.TtoV0, normals), dot(i.TtoV1, normals));

fixed4 matcapLookup = tex2D(_MatCap, capCoord * 0.5 + 0.5);

 matcapLookup.a = 1.0;

 tex *= matcapLookup * 2.0;

 tex.xyz *= i.SHLighting;

 return tex;
 }

ENDCG
 }
 }

FallBack "Mobile/Diffuse"
}

MatCapExamplesMedium

The thing to bear in mind about these, is that they require no dynamic lighting, which means you can show off your artists’ (in this case, the inimitable Mauricio Gartez Ortiz) spectacular bump maps without paying for any lighting calculations.
The version embedded here includes light probe support, so your models will pick up colouring and intensity from your light probes as they move around the level, but there are also a couple of static lightmap version in my PasteBin if you feel like using the shader on a static object.
My last two MatCap examples are there to give you an idea of the detail and visual style achievable with these reasonably simple Shaders.

From that point on, all my Shader work was limited to tweaking and evolving the Shaders I had already developed… and putting together a few others which I can’t imagine would be all that useful to anyone else.  All of these Shaders and whatever else I happen to be working on and be happy enough about to feel like posting will be available on my PasteBin from now on… just FYI.

So there you have it, Renaud’s marvelous Journey into the land of Shaders.  I tried to comment most of them as usefully as I could (particularly since Shaders are woefully commented as a general rule) but please feel free to drop me a note if you have any questions or comments or suggestions (as mentioned above).

Ah yes, and for those of you watching from home : I’m fine.


*  because I think elements that support and reinforce each other are Hot.

NB: I use the C# syntax highlighter in this article because it’s the syntax Unity-users will be most accustomed to seeing on their Shaders, and that seemed like a useful thing to do (given that there are no useful Shader syntax-highlighters that I could find).

DROP A COMMENT

Your email address will not be published. Required fields are marked *