Hello and welcome to the first in what will become a series on esoteric Unity subjects that came up during the making of our game, Placebo Effect. Today, I will talk about my journey into learning how to create customized Unity shaders in HLSL (High Level Shading language). For those unfamiliar with 3d shaders, they are used to control the way surfaces look on 3d objects. Shaders apply colors, textures and image effects to Materials and can create many highly detailed and interesting visual effects in an efficient manner.
- Part I – Creating a basic shader (You are here)
- Part II – Creating a blend shader
- Part III – Extending the blend shader (normal map, metallic map, specular map, etc.) (Coming Soon)
Part I – Creating a basic shader
In our game, we have a variety of organic objects and we wanted them to be able to slowly change to a damage-texture over time based on the object’s health. The goal was to smoothly transition between the non-damage texture to the damaged texture. I assumed that it would be easy to find a shader that would allow me to control the amount of blend between two textures but I didn’t find any suitable ones out there so I decided to write my own (despite having no previous shader knowledge or experience). I wanted to be able to mix textures like this:
Here is how I went about creating the blend shader of my dreams (if you just want to grab the code, skip to Part II). I started by creating a new Unity 3D project named “ShaderTest”. In the new scene that is created by default, I created a capsule to test out our new shaders. I then created a new shader by right-clicking in the project window and choosing:
Create > Shader > Standard Surface Shader
I named the new shader “TestShader”.
As you can see, there are several types of shaders, but all we care about for now is the Standard Surface Shader. Now, create a new Material so we can test out our shader. Select the newly created Material, then choose “Custom/TestShader” from the top of the Inspector panel.
Note how the newly created Material with the new TestShader already has some default parameters shown, such as “Color”, “Abedo (RGB)”, “Smoothness”, and “Metallic”. This is because these properties are built into the default shader template file.
Now, double-click the TestShader file to open it in Visual Studio (or your text-editor of choice) and look at the “Properties” block at the beginning of the file:
Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 }
Notice how these properties match the one’s exposed in the Unity editor, and the names given to them in quotes (“Color”, “Smoothness”, etc.) become the property descriptions in the Unity editor. (For more info on writing surface shaders, go here).
Standard Surface Shader Basics
For the simplicity of our discussion we are going to remove all of this automatically generated code and strip our shader down to the basics. Let’s start by creating a shader that does nothing but control the surface color of an object, the complete shader will look like this:
Shader "Custom/TestShader2" { Properties { _Color ("My Color", Color) = (1,1,1,1) // The input color exposed in the Unity Editor, defined as type "Color" and set to rgba 1,1,1,1 (solid white) } SubShader { Tags { "RenderType"="Opaque" } CGPROGRAM // Physically based Standard lighting model, // enable shadows on all light types #pragma surface surf Standard // This Input struct is required for the "surf" function struct Input { float2 x; }; fixed4 _Color; // A variable to store rgba color values // This "surf" surface function with this signature is required // It is executed for each pixel of the objects with this shader void surf (Input IN, inout SurfaceOutputStandard o) { // Albedo comes is tinted by a color fixed4 c = _Color; // Get the _Color value o.Albedo = c.rgb; // Set the "Albedo" or diffuse color rgb } ENDCG } FallBack "Diffuse" // Revert to legacy shader if this shader-type not supported }
If you copy this code into a file named TestShader2.shader, and then apply this new “TestShader2” shader to a new Material, you will see now a simplified set of properties in the Inspector. You can see that only the “Color” property is exposed.
Let’s break down each part of this shader file to understand what is happening. The first part of the file defines a new Shader, and the part in quotes informs you of the Shader’s name and folder location in the Material’s Shader selection drop-down.
Shader "Custom/TestShader2" { }
Next comes the “Properties” section which defines input properties for the shader. Our shader exposed a variable named _Color with the type of fixed4. This datatype hold four fixed floating point values representing a pixel’s Red, Blue, Green, and Alpha color channels.
Properties { // The input color exposed in the Unity Editor, defined // as type "Color" and set to rgba 1,1,1,1 (solid white) _Color ("My Color", Color) = (1,1,1,1) }
Next comes the Subshader block. Every surface shader has its code placed inside a “SubShader” block.
SubShader { }
Now, we have:
Tags { "RenderType"="Opaque" }
This tells the rendering engine the z-depth order in which to render this object, as unity renders opaque objects, transparent objects, and effects in a specific order. When dealing with transparent objects, the rendering engine needs to know that these get rendered after opaque objects at the same z-depth. For our purposes today, we will use Opaque.
Next, comes this structure:
CGPROGRAM .. ENDCG
The core of the shader will be in this CGPROGRAM .. ENDCG
block.
Then we have the #pragma surface directive:
#pragma surface surf Standard
The pragma keyword tells the compiler it is about to receive some special instructions. The #pragma surface directive has the following form:
#pragma surface surfaceFunction lightModel [optionalparams]
Note the surface keyword at the beginning and then the definition of the surfaceFunction. In our shader, we have defined this function name here as “surf” and then defined the “surf()” function down below. The other required parameter is the lightModel, which can be of type Standard, StandardSpecular, BlinnPhong, Lambert, or a custom model that you provide (more information on these properties is available here).
Next, we create the “Input” struct which is required for our “surf()” function. I define an x variable here because I need something just to create the struct, this x variable is not actually used.
struct Input { float2 x; };
Next we need a definition of the _Color variable, which is needed to take the color value _Color from the input properties at the top. These variable names must match.
fixed4 _Color;
Now we come to the fun part, the “surf()” function that applies color to each pixel.
void surf (Input IN, inout SurfaceOutputStandard o) { ... }
You can see we have the “Input” struct being passed in as well as the “o” output parameter which we will use inside the function to control the surface color.
We have now reached the meat of the shader, the code inside the surface function:
fixed4 c = _Color; // Get the _Color value o.Albedo = c.rgb; // Set the "Albedo" or diffuse color rgb
As you can see, we simply get the “_Color” variable’s value and the apply the RGB component to the “o” variable’s “Albedo” property which controls unlit surface color. The “Standard” surface shader’s output object has the following properties:
fixed3 Albedo; // base (diffuse or specular) color fixed3 Normal; // tangent space normal, if written half3 Emission; // Emission (surface lighting) color half Metallic; // 0=non-metal, 1=metal half Smoothness; // 0=rough, 1=smooth half Occlusion; // occlusion (default 1) fixed Alpha; // alpha for transparencies
As you can see, we have just barely scratched the (wait for it…) surface in writing Unity Standard Surface Shaders, but I hope this was a good introduction to the basics. Next time, we will look at adding a bitmap texture to this shader and then blending between two bitmaps in the same shader.
Greetings! This is my first comment here so I just wanted to give a quick shout out and say I genuinely enjoy reading your posts. Can you suggest any other blogs/websites/forums that go over the same topics?
Thanks a lot!
Sure, take a look at this info from the Unity Team, A Gentle Introduction to Shaders.
WOW just what I was searching for! This is great info, thanks.
Good way of telling, and nice paragraph to take
information regarding my presentation topic, which
i am going to deliver in school.