Writing Shaders 3 : Surface Shaders

Last time, I covered how to write a simple vertex lit shader influenced by a single light source. Adding support for more lights with the standard CG shader code can be extremely tedious, and this is where Unity provides a fantastic solution: Surface shaders. Surface shaders automate much of the process so you can implement a complex lighting model without writing a lot of repetitive code.

To get started with surface shaders, create a new shader in your project, and open it. You will have a file that looks very similar to this:

Shader “Custom/SurfaceShaderSample” {

Properties {

_MainTex (“Base (RGB)”, 2D) = “white” {}

}

SubShader {

Tags { “RenderType”=”Opaque” }

LOD 200

 

CGPROGRAM

#pragma surface surf Lambert

sampler2D _MainTex;

struct Input {

float2 uv_MainTex;

};

void surf (Input IN, inout SurfaceOutput o) {

half4 c = tex2D (_MainTex, IN.uv_MainTex);

o.Albedo = c.rgb;

o.Alpha = c.a;

}

ENDCG

}

FallBack “Diffuse”

}

The first important change here, that is really easy to miss, is the shader logic sits in the SubShader, there is no Pass. This is a limitation of surface shaders, when Unity compiles this shader file, it will automatically generate multiple passes as necessary. It does make writing a multipass shader that uses surface shaders difficult. The second change in this file is the #pragma surface surf Lambert. In the previous shader posts, we used #pragma to define the vertex and fragment shader programs. The #pragma surface command tells Unity that this is a surface shader, and defines the surface shader program, as well as the lighting model. There are two built in lighting models you can use, Lambert and BlinnPhong. Lambert is the diffuse lighting model we have been working with so far, and BlinnPhong is the specular lighting model that makes things appear shiny. You can also specify some optional parameters at the end of the #pragma surface line to further configure the shader.

The surface shader program functions very similar to the fragment programs from previous lessons. The surface shader program will contain all of your shader logic except for the lighting behavior. The output of this is a little different, aside from the obvious change to modifying an inout variable instead of returning a color value, you have probably noticed you are setting a value called “Albedo” instead of one labeled color.

Chances are, if you are reading this shader tutorial you are not very familiar with lighting terminology. Albedo is also known as the “reflection coefficient”, and it is the diffuse reflecting power of a surface. A simpler description, within the context of writing these shaders, is the albedo is the diffuse color of an object, the color it will be under full, white light.

The final change in this file is the fallback near the end. With modern game development, and multiplatform tools like Unity, very few developers are going to willingly limit their game to a single piece of hardware, often the farthest you might go is limiting your game to a single platform. Even within a single platform, such as iOS, there is a range of hardware and graphics processors. Specifying a fallback gives Unity the information to know what shader to substitute to on platforms that cannot run the shader.

Once you understand these basics of surface shaders, then you should be able to understand the wonderful Unity documentation and example code on the subject. I’m going to give you a few links to read, and the rest of this blog post will cover terminology Unity uses but does not define. This is the base page on surface shaders for Unity, and will be your primary reference point http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaders.html. This page goes through the process of writing custom lighting models for surface shaders http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaderLightingExamples.html. This page covers everything except the lighting models for surface shaders http://docs.unity3d.com/Documentation/Components/SL-SurfaceShaderExamples.html.

The first set of terms to define are those contained in the SurfaceOutput structure on the reference page. I’ve already defined Albedo. If you have somehow forgotten what Normal means, it is the facing of the vertex / fragment being rendered, and is the core piece of data for computing how much a given light is effecting the fragment. Emission is the color the object will be in the absence of light. Specular and Gloss both modify the specular highlight, Gloss effects the size, and Specular effects the intensity. Specular highlights are the bright spots that appear on shiny objects. The Alpha value contains the transparency of the object, if the object is setup to be transparent.

In the lighting examples page, the concept of subsurface scattering is mentioned in the diffuse wrap example. Subsurface scattering is when light enters the surface of a translucent object, bounces around under the surface, and exits at another point. Human skin is a real world material affected by subsurface scattering, which is why it is something often simulated in shader logic. Wrapped diffuse lighting is one way of faking subsurface scattering. In the example on the Unity page, what they mean by wrapped is the shift from -1 to 1 for the normal value of the lighting to 0 to 1, by halving the dot product between the normal and light direction, and adding 0.5 to it.

The toon ramp example covers a very important shader concept: using textures as a way to pass data into a shader. Shader programs are very performance demanding, and doing any complex calculation can cause a shader to drop the framerate of your game. An important thing to note when pulling a color value out of a texture with a shader is you index into the texture’s horizontal and vertical with a 0 to 1 value. This means that any time you have math in your shader logic that maps nicely to a percentage, such as the previously mentioned wrapped diffuse value, you can use this value to look up information in a texture. Generally these data textures are treated by shader logic as one dimensional, even though they are generally going to be something like 256 pixels wide by 8 pixels tall, or a square 256×256 due to texture restrictions. In the toon ramp example, they index into a texture, which is the gradient seen below the two sample pictures. You can get creative with this, and create interesting effects by changing the input texture from a smooth gradient to a really blocky gradient, you can create a very cartoony looking effect. Here is an example of this, using the sample toon ramp shader: http://i.imgur.com/BBrERWS.png.

On the surface shaders example page, one example generates rim lighting by setting the emission to some math that generates a rim value. The rim of an object is defined by the fragments that are pointing at a 90 degree angle away from the camera’s view direction. The saturate function clamps a value within the range of 0 to 1. Taking the dot product of the camera’s view direction and the normal of the fragment and saturating it, we get a 0 to 1, with 1 being fragments facing directly at the camera, and 0 being fragments facing 90 degrees away. Flipping this value, by subtracting it from 1, gives us math that defines the rim of an object, with anything at value 1 facing a 90 degree angle away from the camera, and anything at 0 facing towards the camera.

This rim value allows us to simulate Fresnel reflection. Think of a soap bubble, and how, even though you can easily see through the middle of it, you can also clearly see a circular outline for it, no matter what angle you look at it from. When light exits a partially transparent substance, such as water, the direction the light is moving at changes due to refraction. If the angle of the light is past what is called the critical angle, then the light will not exit the medium, and will instead reflect within the medium. Fresnel equations are used to figure out how much light is refracted, versus how much light is reflected. So, for an object like a bubble, you can see the edges more clearly because you are seeing the light that has reflected internally instead of immediately refracting out of the object. We simulate this effect in shader logic by making an object more opaque the closer to the rim, and more transparent the more the fragment is facing the camera.

This is a simple shader that makes a fragment more opaque the closer to the rim it is, and you can see it in action here: http://i.imgur.com/28Cl7aY.png.

Shader “Custom/SurfaceShaderSample” {

Properties {

_MainTex (“Texture”, 2D) = “white” {}

}

SubShader {

Tags {“Queue”=”Transparent” “IgnoreProjector”=”True” “RenderType”=”Transparent”}

Blend SrcAlpha OneMinusSrcAlpha

 

CGPROGRAM

#pragma surface surf Lambert finalcolor:mycolor

struct Input {

float2 uv_MainTex;

float3 viewDir;

};

sampler2D _MainTex;

 

void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)

{

half rim = 1 – saturate(dot (normalize(IN.viewDir), o.Normal));

color.a = rim;

}

 

void surf (Input IN, inout SurfaceOutput o) {

o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;

}

ENDCG

}

}

The first new thing in this block of shader code is the logic to put this shader into the transparent rendering queue, and inform the renderer how to blend the pixels from this shader against the pixels it is rendering in front of. The next new thing is the use of a final color. This is where you can do any logic after the lighting is applied. I could have done the rim math within the surface function, but in a later example I am going to also adjust the color here.

Earlier, in the section on the toon shader, I mentioned how powerful using textures as data can be. This Fresnel shader logic we have generates a 0 to 1 value, and my favorite thing to do with percentage values like this is to pull data out of a texture.

Shader “Custom/SurfaceShaderSample” {

Properties {

_MainTex (“Texture”, 2D) = “white” {}

_Ramp (“Shading Ramp”, 2D) = “gray” {}

}

SubShader {

Tags {“Queue”=”Transparent” “IgnoreProjector”=”True” “RenderType”=”Transparent”}

Blend SrcAlpha OneMinusSrcAlpha

 

CGPROGRAM

#pragma surface surf Lambert finalcolor:mycolor

struct Input {

float2 uv_MainTex;

float3 viewDir;

};

sampler2D _MainTex;

sampler2D _Ramp;

 

void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)

{

half rim = 1 – saturate(dot (normalize(IN.viewDir), o.Normal));

half4 rimCol = tex2D (_Ramp, float2(rim,0));

color.a = rimCol.a;

}

 

void surf (Input IN, inout SurfaceOutput o) {

o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;

}

ENDCG

}

}

Indexing into the texture using the rim value gives us a lot of control over this shader, and can produce many different effects http://i.imgur.com/9cvv0Eg.png, http://i.imgur.com/MZKekMe.png and http://i.imgur.com/NvatwjG.png.

The next iteration of this shader, I am going to mix the color of the ramp texture with the color of the main texture. This lets us give a really strong glow outline to the edge of the rim, as seen here http://i.imgur.com/PneclFt.png.

Shader “Custom/SurfaceShaderSample” {

Properties {

_MainTex (“Texture”, 2D) = “white” {}

_Ramp (“Shading Ramp”, 2D) = “gray” {}

}

SubShader {

Tags {“Queue”=”Transparent” “IgnoreProjector”=”True” “RenderType”=”Transparent”}

Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma surface surf Lambert finalcolor:mycolor

struct Input {

float2 uv_MainTex;

float3 viewDir;

};

sampler2D _MainTex;

sampler2D _Ramp;

float _RampOffset;

void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)

{

half rim = 1 – saturate(dot (normalize(IN.viewDir), o.Normal));

half4 rimCol = tex2D (_Ramp, float2(rim,0));

color.rgb = color.rgb * (1-rimCol.a) + rimCol.rgb * (rimCol.a);

color.a = rimCol.a;

}

void surf (Input IN, inout SurfaceOutput o) {

o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;

}

ENDCG

}

}

So far we have been working with static, unanimated data. Unity’s animation tools allow us to animate any shader property we want. So we’re going to add a new property as an offset for the ramp lookup, and animated that, which will give us this effect http://i.imgur.com/BvOLVng.gif.

Shader “Custom/SurfaceShaderSample” {

Properties {

_MainTex (“Texture”, 2D) = “white” {}

_Ramp (“Shading Ramp”, 2D) = “gray” {}

_RampOffset (“Ramp Offset”, Range(0.0,1.0)) = 0.0

}

SubShader {

Tags {“Queue”=”Transparent” “IgnoreProjector”=”True” “RenderType”=”Transparent”}

Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma surface surf Lambert finalcolor:mycolor

struct Input {

float2 uv_MainTex;

float3 viewDir;

};

sampler2D _MainTex;

sampler2D _Ramp;

float _RampOffset;

void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)

{

half rim = 1 – saturate(dot (normalize(IN.viewDir), o.Normal));

half4 rimCol = tex2D (_Ramp, float2(rim+_RampOffset,0));

color.rgb = color.rgb * (1-rimCol.a) + rimCol.rgb * (rimCol.a);

color.a = rimCol.a;

}

void surf (Input IN, inout SurfaceOutput o) {

o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;

}

ENDCG

}

}

That is the basics of surface shaders. Hopefully you’re ready at this point to start experimenting and constructing your own shaders in Unity.

Advertisements
About

Joseph Stankowicz is a software engineer who has worked in the video games industry for over eight years. The last two years have had a heavy focus on Unity development, where he helped ship over eleven titles to iOS and Android platforms. He also is really excited about 3D printing, and keeps his Solidoodle 3 printing out stuff as often as possible. You can view his LinkedIn profile here http://www.linkedin.com/pub/joseph-stankowicz/60/294/420

Posted in Unity3D
One comment on “Writing Shaders 3 : Surface Shaders
  1. Bornfirst says:

    Very nice tutorial

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: