in programming, shader, tutorial, Unity3D

Surface shaders in Unity3D

Share Button

Part 1, Part 2, Part 3, Part 4, Part 5

This is the second part of a series of posts on Unity3D shaders, and it will focus on surface shaders. As previously mentioned, shaders are special programs written in a language called Cg / HLSL which is executed by GPUs. They are used to draw triangles of your 3D models on the screen. Shaders are, in a nutshell, the code which represents how different materials are rendered. Surface shaders are introduced in Unity3D to simplify the way developers can define the look of their materials.

Surface shader

The diagram above loosely shows how a surface shader works. The 3D model is firstly passed to a function which can alter its geometry. Then, it is passed (together with other information) to a function which defines its “look” using some intuitive properties. Finally, these properties are used by a lighting model to determine how the geometry will be affected by the nearby light sources. The result are the RGBA colours of every pixel of the model.

The surface function

The heart of a surface shader is its surface function. It takes data from the 3D model as input, and outputs its rendering properties. The following surface shader gives an object a diffuse white colour:

shader_01

Line 5 specifies that the surface function for this shader is surf and that a Lambertian lighting model should be used. Line 10 indicates that the albedo of the material, that is its base colour, should be white. The surface function doesn’t use any data from the original 3D model; despite this, Cg / HLSL requires a input struct to be defined.

Surface output

The struct SurfaceOutput has several other properties which can be used to determine the final aspect of a material:

  • fixed3 Albedo: the base colour / texture of an object,
  • fixed3 Normal: the direction of the face, which determines its reflection angle,
  • fixed3 Emission: how much light this object is generating by itself,
  • half Specular: how well the material reflects lights from 0 to 1,
  • fixed Gloss: how diffuse the specular reflection is,
  • fixed Alpha: how transparent the material is.

Cg / HLSL supports the traditional  float type, but you’ll rarely need a 32 bit precision for your calculations. When 16 bits are enough, the  half type is usually preferred. Since the majority of parameters have a range which goes from 0 to 1, or -1 to +1, Cg supports the fixed type. It spans at least from -2 to +2, and it uses 10 bits.

For all these types, Cg / HLSL also supports packed arrays:  fixed2, fixed3, and  fixed4. These types are optimised for parallel computation, so that most of the common operations can be performed in just one instruction. These four, for examples, are equivalent:

Sampling textures

Adding a texture to to a model is slightly more complicated. Before showing the code, we need to understand how texture mapping works in 3D objects. Every textured model is made up of several triangles, each one made of three vertices. Data can be stored in these vertices. They typically contain UV and color data. UV is a 2D vector which indicates which point of the texture is mapped to that vertex.

soldier

In the picture above you can see the model of a soldier from the Unity3D Bootcamp demo. It is rendered as shaded wireframe. Two triangles have been highlighted on the model: their vertices yield a couple of numbers. They are the Cartesian coordinates, normalised from 0 to 1, which map to the texture on the right. These numbers are the UV coordinates.

If we want to associate a texture to a 3D object, we need the UV data of its vertices. The following shader maps a texture onto a model, according its UV model.

shader_02

The property _MainText is a texture which is declared in line 12 and made accessible from the material inspector in line 3. The UV data of the current pixel is gathered in line 10; this is done by naming a field of the Input  struct as uv followed by the name of the texture ( uv_MainText, in this case).

The next step is to find the part of the texture which UV refers to. Cg / HLSL provides a useful function for this, called tex2D: given a texture and some UV coordinate, it returns the RGBA colour. tex2D takes into account other parameters which can be set directly from Unity3D, when importing the texture.

It is important to remember that the UV coordinate are stored only in the vertices. When the shader evaluates a pixel which is not a vertex, the function  tex2D interpolates the UV coordinates of the three closest vertices.

Surface input

Cg / HLSL has some other interesting, non-obvious features. The surface input,  Input, can be filled with values which Unity3D will calculate for us. For instance, adding float3 worldPos will be initialised with the world position of the point surf is elaborating. This is often used to creates effects which depends on the distance from a particular point.

shader_03

Lines 20-21 calculated the distance of the pixel being drawn, IN.worldPos, from the point we’ve defined in the material inspector, _Center. Then, it clamps it between zero and one, so that is one in _Center and fades to zero at _Radius. If the distance falls within a certain range, pixels are coloured white.

Is worth noting that shaders are executed on GPUs, which are highly optimised for sequential code. Adding branches to a shader can dramatically lower its performance. It is often more efficient to calculate both branches and mixing the results:

Cg / HLSL has lot of built-in functions, such as saturate and step, which can easily replace the majority of if statements.

Other inputs

Cg allows to use several other special fields such as worldPos; here’s a list of the most used according to Unity3D official documentation:

  • float3 viewDir: the direction of the camera (view direction);
  • float4 name : COLOR: by using this syntax, the variable name will contain the colour of the vertex;
  • float4 screenPos: the position on the current pixel on the screen;
  • float3 worldPos: the position of the current pixel, in world coordinates.

Vertex function

Another interesting feature of surface shaders is the ability to change vertices before sending them to surf. While surf manipulates colours in the RGBA space, to use a vertex modifier function you need to learn how to manipulate 3D points in space. Let’s start with an easy example: a shader which makes a 3D model …chubbier. To do this, we have to expand the triangles along the direction they’re facing (think about a balloon which is inflated). The direction of a triangle it’s given by its normal, which is a unit vector perpendicular to the surface of the triangle itself. If we want to extend its vertices in the direction of its normal, what we have to do it:

    \[newVertex = vertex + normal * amount\]

where amount indicates how much the new vertex will be displaced from the previous one. This technique is called normal extrusion.

soldier.gif

Line 9 specifies that there is a vertex modifier, called vert. It takes the position of a vertex and it projects it along its normal. appdata_full is a struct which contains all the data of the current vertex.

Putting all together: the snow shader

A typical surface shader which uses both surf and vert is the infamous snow effect, which appeared in several blogs, each time with a slightly different flavour. It simulates the accumulation of snow on the triangles of a model. Initially only the triangles directly facing _SnowDirection are affected. By increasing _Snow, even the triangles which are not oriented towards the sky are eventually affected.

dot product (2)First of all, we need to understand when a triangle is oriented towards the sky. The direction the snow is coming from, _SnowDirection, will be a unit vector as well. There are many ways in which we can check how aligned they are, but the easiest one is projecting the normal onto the snow direction. Since both vectors have length one, the resulting quantity will be bounded between +1 (same direction) and -1 (opposite direction). We’ll encounter this quantity again in the next tutorial, but for now you just have to know that is known as dot product and equal to cos(\theta). Asking the dot product to be greater then a certain value _Snow, means that we are only interested in the normals which differ, in direction, less then \theta. For instance:

  • cos(\theta)\geq +1 is true only when the two directions are the same;
  • cos(\theta)\geq 0 is true when \theta is less then 90 degrees;
  • cos(\theta)\geq -1 is always true.

There’s another little piece of information which is needed. While _SnowDirection represents a direction expressed in world coordinates, normals are generally expressed in object coordinates. We cannot compare these two quantities directly, because they are mapped to different coordinate systems. Unity3D provides a function called WorldNormalVector which can be used to map normals into world coordinates.

soldier_4

Rather the manually calculating the cosine of the angle between the vector, Cg has a very efficient implementation of the dot product called dot.

Line 36 utilises a different method to convert the normal into world coordinates. The function  WorldNormalVector is in fact not available in the vertex modifier.

If you really need snow in your game, consider buying something more advanced, like Winter Shaders.

Conclusion

This post introduces surface shaders and shows how they can be used for a variety of effects. This post was inspired by the Surface Shader Examples page in the Unity3D manual. There is also another page which explains in details how to implement other lighting models. That topic will be explored in details in the third part of this tutorial.


Support this blog! ♥

For the past three years I've been dedicating more and more of my time to the creation of quality tutorials, mainly about game development and machine learning. If you think these posts have either helped or inspired you, please consider supporting this blog.

Paypal
Twitter_logo

Don't miss the next tutorial!

There's a new post every Wednesday: leave your email to be notified!


Write a Comment

Comment

16 Comments

  1. You say GPUs are highly optimized for sequential code, but that sounds very ambiguous.

    GPUs are highly optimized for parallel code, and that is exactly why branching is not as cheap as on a CPU. The same code is executed on a whole bunch of fragments at the same time (a group called a warp or a wavefront), it generally needs to execute both sides of the branch, and throw away the result of the side of the branch that does not apply. So it’s more that you can’t optimize away a bunch of code with an if statement.

    Another part of that has to do with pipelining: if work can be predicted (such as a texture lookup), it can sometimes be done at the same time as other work. But if your texture lookups depend on the output of a calculation, that’s not quite as straightforward.

  2. Hey, it works! Look at that!

    Thanks for the tutorials. You’re definitely filling a major gap in what’s available out there.

    • Hey! In fact, it cannot be! That example refers directly to the line of code which is:

      if(dot(v.normal, sn.xyz) >= _Snow)

      In that line, the dot product is, de facto, the cosine function. If you place _Snow = +1, you get:

      if (cos(theta) >= +1)

      which is true for one value of theta only.

  3. i couldnt understand this part when i was reading “surface function”. First it said surface function (it takes data from 3dmodel as input) then later it said it(surf funtion) doesnt use any data from 3d model inspite of that an input struct has to be defined..i might be wrong somewhere but if you could explain it a bit?

    i wouldve attached an image with lines highlighted for your convinience but it doesnt allow me to attach image.

    • The first part means surface shaders *in general* will take 3D model data as inputs. The second part means the specific surf function in the specific example in the article didn’t actually need any of the model data, but the language requires declaring the input value anyway.

  4. Hey! Thanks a lot for your tutorial series – it’s much appreciated.

    I’m a bit confused about the following lines:

    // Convert the normal to world coordinates
    float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
    if(dot(v.normal, sn.xyz) >= _Snow)
    […]

    According to http://forum.unity3d.com/threads/_object2world-or-unity_matrix_it_mv.112446/, “UNITY_MATRIX_IT_MV is for rotating normals from object into eye space”. But we don’t want to convert _SnowDirection from object to eye space – we want to convert it from world to object space, right? (And the comment seems to be totally misplaced as the normal isn’t converted at all.)

    • To make it work you should actually do:

      float4 sn = mul(transpose(_Object2World), _SnowDirection);

      Which converts the normal from object to world coordinates.

  5. I think there is a typo.

    In vert shader of the code for Snow Shader, you are saying “// Convert the normal to world coordinates” in the comment.

    // Convert the normal to world coordinates
    float4 sn = mul(_SnowDirection, _World2Object);

    But isn’t it actually converting the world coordinate vector which is _SnowDirection into object coordinate?
    So, sn is not object coordinate snow direction and not can be compare with appdata_full.normal which is object coordinate.

    Please correct me if I am wrong.

  6. Hi, I too have the same doubt as Jae . Are we converting snow direction from world space to object space here:

    // Convert the normal to world coordinates
    float4 sn = mul(_SnowDirection, _World2Object);

    so, that we can compare with v.normal which is already in object space.

    • I don’t understand the line after the comment “// Convert the normal to world coordinates” either.
      As Ivan wrote, it is equivalent to float4 sn = mul(transpose(_Object2World), _SnowDirection);
      But why the transpose?

      • Hi!
        Sorry for the confusion!

        _SnowDirection is in world space.

        In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

        In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

        I have corrected the comment (which said the opposite).

        Does it make sense to you know?

Webmentions

  • LCD Display Shader Effect – Alan Zucconi June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • Unity3D: Tutoriais e Documentação de Shaders | June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • Passing arrays to a shader: heatmaps in Unity3D - Alan Zucconi June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • Postprocessing and image effects in Unity - Shader Tutorial June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • Vertex and fragment shaders in Unity3D | Alan Zucconi June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • Physically Based Rendering and lighting models in Unity3D | Alan Zucconi June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • /r/GameDev 6/18 Roundup | Kevin Stubbs June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?

  • A gentle introduction to shaders in Unity3D | Alan Zucconi June 20, 2017

    Hi!
    Sorry for the confusion!

    _SnowDirection is in world space.

    In the surface function, I convert o.Normal from object to world space, so it can be compared with _SnowDirection.

    In the vertex function, I convert _SnowDirection from world to object space, so it can be compared with v.Normal.

    I have corrected the comment (which said the opposite).

    Does it make sense to you know?