Learn to write surface shaders in Unity in this complete tutorial series.
In this Unity tutorial series we will be giving you an introduction shader scripting, showing you how to write your our own surface shaders for your Unity game.
This text tutorial is accompanied by a complete video. I recommend watching the video and referring to the text as reference.
Be sure to read the tutorial carefully, and post any questions you may have if you get stuck, as there is a lot to cover. I will be using some examples from the Unity shader reference found here:
http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaderExamples.html
My goal is to help you understand how shaders work and how you can write your own shaders from scratch in Unity.
NOTE: This tutorial requires very basic knowledge of scripting such as variables and functions. If you have no scripting experience I recommend you first watch our Introduction to Scripting tutorial in the getting started section.
In part 5 we took a look at adding specularity to our shaders, now let’s take that a step further and add cubemap reflections.
Part 6 – Adding cubemap reflections to our Unity surface shaders
Instead of specularity, perhaps you want an object to be reflective such as a window, plastic or a teapot. This is where cubemaps come into play.
So let’s take our lambert diffuse shader and add some reflections:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Shader "unityCookie/Cubemap Reflection" { Properties { _MainTex ("Diffuse Texture", 2D) = "white" {} _Cube ("Cube Map", CUBE) = "" {} //A property so we can add the cube map in Unity } Subshader { Tags { "RenderType" = "Opaque"} CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float3 worldRefl; //This is a pre-defined variable in unity for world space reflections }; sampler2D _MainTex; samplerCUBE _Cube; //We sample the cubemap so we can use it void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Emission = texCUBE (_Cube, IN.worldRefl).rgb; } ENDCG } Fallback "Diffuse" } |
We introduce four new commands.
- worldRefl; This is a built in variable which allows you to display an image in world space, making it appear as if the material is reflecting.
- samplerCUBE This is like the sampler2D, but is specific to sampling cube maps, this tells Unity the map is a cubemap.
- o.Emission This makes the surface emit this color, which is important for lighting and refelctions. It does not mean that the surface is actually emitting light.
- texCUBE Like tex2D, but instead of a flat image we want to treat this like a box.
So this is great, but what about the normal map? How do we get that to work? Well normal maps are a bit more tricky, we need to use the INTERNAL_DATA to get the WorldReflectionVector of our normal. Sounds confusing, but it isn’t much more to add to our code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Shader "unityCookie/Bumped Cubemap Reflection" { Properties { _MainTex ("Diffuse Texture", 2D) = "white" {} _BumpTex ("Normal Map", 2D) = "bump" {} _Cube ("Cube Map", CUBE) = "" {} } Subshader { Tags { "RenderType" = "Opaque"} CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpTex; float3 worldRefl; //We don't use this down below, but it is used in the INTERNAL_DATA module so we still need to specify we want it. INTERNAL_DATA // Notice how this is in all caps, this is telling Unity we want to use the internal data module, we won't get into what that is but just know that it's got some cool stuff in it. }; sampler2D _MainTex; sampler2D _BumpTex; samplerCUBE _Cube; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Normal = UnpackNormal (tex2D (_BumpTex, IN.uv_BumpTex)); o.Emission = texCUBE (_Cube, WorldReflectionVector( IN, o.Normal)).rgb; //We take the world reflection vector from our normals, and we simply give it all our inputs } ENDCG } Fallback "Diffuse" } |
Okay so hopefully that’s not too confusing. The WorldReflectionVector is a built in command that takes our normals and world space reflection, then uses the internal data to figure out what the reflection should be according to the normals.
Whew, this stuff get’s tricky doesn’t it? But don’t worry, once you understand it, this stuff will be a piece of cake.
In the next step we will move on to rim lighting.


Any recommended sites for cube map texture?
There is a bunch built into unity but you can find a lot on google, also I found a lot on polycount, will see if I can find a link.
-Alex
One more question: Is cube map different from skybox?
Not really, they are fairly interchangeable terms
-Alex
Hi Alex, in the Video, at 11’26” the Audio restarts as at the beginning of the Video ( Hey guys bla bla… ), over what you might be saying according to what you’re doing and showing… Or is it a test to make sure we’re not sleeping
That is really weird, thanks for the heads up, it doesn’t do it on my local version so time to re-upload to youtube
Fixed!
Got it on youtube thanks. You might also fix it in the download files for Citizens
The audio of the video file in the citizen download is still broken, I’m afraid. If you could get around to reuploading the video I’d really appreciate it.
Aside from that, this series is really great and helped me quite a bit in understanding Unity-shaders.
Ah yes sorry guys, will try and get onto this as soon as possible
I have a question of the surface shaders.
I’m making a game in unity where there is object that has surface shader (one close to the one in the first part of your tutorial series.) And the point is to have object that glows evenly for player to notice it and to click it (and then something happens.) But my question is for surface shaders is it possible to make the power element slide evenly? Or is it that when the game is baked it only calculates the surface shader once and it can’t render it in real time after that?
It seems like this is hard question(i think) since I haven’t found answer to it from anywhere.
Hi Emi
Pow is an exponential function, so will never go evenly, but you can control this with javascript using an inverse pow to even the effect.
This leads to your next question, yes you can control your shaders using javascript with the renderer.sharedMaterial, we will cover this in a future series.
-Alex