18 min read

In this article by Alan Zucconi, author of the book Unity 5.x Shaders and Effects Cookbook, we will see that the term shader originates from the fact that Cg has been mainly used to simulate realistic lighting conditions (shadows) on three-dimensional models. Despite this, shaders are now much more than that. They not only define the way objects are going to look, but also redefine their shapes entirely. If you want to learn how to manipulate the geometry of a three-dimensional object only via shaders, this article is for you.

In this article, you will learn the following:

  • Extruding your models
  • Implementing a snow shader
  • Implementing a volumetric explosion

(For more resources related to this topic, see here.)

In this article, we will explain that 3D models are not just a collection of triangles. Each vertex can contain data, which is essential for correctly rendering the model itself. This article will explore how to access this information in order to use it in a shader. We will also explore how the geometry of an object can be deformed simply using Cg code.

Extruding your models

One of the biggest problems in games is repetition. Creating new content is a time-consuming task and when you have to face a thousand enemies, the chances are that they will all look the same. A relatively cheap technique to add variations to your models is using a shader that alters its basic geometry. This recipe will show a technique called normal extrusion, which can be used to create a chubbier or skinnier version of a model, as shown in the following image with the soldier from the Unity camp (Demo Gameplay):

 Unity 5.x Shaders and Effects Cookbook - Second Edition

Getting ready

For this recipe, we need to have access to the shader used by the model that you want to alter. Once you have it, we will duplicate it so that we can edit it safely. It can be done as follows:

  1. Find the shader that your model is using and, once selected, duplicate it by pressing Ctrl+D.
  2. Duplicate the original material of the model and assign the cloned shader to it.
  3. Assign the new material to your model and start editing it.

For this effect to work, your model should have normals.

How to do it…

To create this effect, start by modifying the duplicated shader as shown in the following:

  1. Let’s start by adding a property to our shader, which will be used to modulate its extrusion. The range that is presented here goes from -1 to +1;however, you might have to adjust that according to your own needs, as follows:
    _Amount ("Extrusion Amount", Range(-1,+1)) = 0
  2. Couple the property with its respective variable, as shown in the following:
    float _Amount;
  3. Change the pragma directive so that it now uses a vertex modifier. You can do this by adding vertex:function_name at the end of it. In our case, we have called the vertfunction, as follows:
    #pragma surface surf Lambert vertex:vert
  4. Add the following vertex modifier:
    void vert (inout appdata_full v) {
    v.vertex.xyz += v.normal * _Amount;
    }
    
  5. The shader is now ready; you can use the Extrusion Amount slider in the Inspectormaterial to make your model skinnier or chubbier.

How it works…

Surface shaders works in two steps: the surface function and the vertex modifier. It takes the data structure of a vertex (which is usually called appdata_full) and applies a transformation to it. This gives us the freedom to virtually do everything with the geometry of our model. We signal the graphics processing unit(GPU) that such a function exists by adding vertex:vert to the pragma directive of the surface shader.

One of the most simple yet effective techniques that can be used to alter the geometry of a model is called normal extrusion. It works by projecting a vertex along its normal direction. This is done by the following line of code:

v.vertex.xyz += v.normal * _Amount;

The position of a vertex is displaced by the_Amount units toward the vertex normal. If _Amount gets too high, the results can be quite unpleasant. However, you can add lot of variations to your modelswith smaller values.

There’s more…

If you have multiple enemies and you want each one to have theirown weight, you have to create a different material for each one of them. This is necessary as thematerials are normally shared between models and changing one will change all of them. There are several ways in which you can do this; the quickest one is to create a script that automatically does it for you. The following script, once attached to an object with Renderer, will duplicate its first material and set the _Amount property automatically, as follows:

using UnityEngine;
publicclassNormalExtruder : MonoBehaviour {

  [Range(-0.0001f, 0.0001f)]
publicfloat amount = 0;

// Use this for initialization
void Start () {
Material material = GetComponent<Renderer>().sharedMaterial;
Material newMaterial = new Material(material);
    newMaterial.SetFloat("_Amount", amount);
    GetComponent<Renderer>().material = newMaterial;
  }
}

Adding extrusion maps

This technique can actually be improved even further. We can add an extra texture (or using the alpha channel of the main one) to indicate the amount of the extrusion. This allows a better control over which parts are raised or lowered. The following code shows how it is possible to achieve such an effect:

sampler2D _ExtrusionTex;
void vert(inout appdata_full v) {
float4 tex = tex2Dlod (_ExtrusionTex, 
float4(v.texcoord.xy,0,0));
float extrusion = tex.r * 2 - 1;
v.vertex.xyz += v.normal * _Amount * extrusion;
}

The red channel of _ExtrusionTex is used as a multiplying coefficient for normal extrusion. A value of 0.5 leaves the model unaffected; darker or lighter shades are used to extrude vertices inward or outward, respectively. You should notice that to sample a texture in a vertex modifier, tex2Dlod should be used instead of tex2D.

In shaders, colour channels go from 0 to 1.Although, sometimes, you need to represent negative values as well (such as inward extrusion). When this is the case, treat 0.5 as zero; having smaller values as negative and higher values as positive. This is exactly what happens with normals, which are usually encoded in RGB textures. The UnpackNormal()function is used to map a value in the (0,1) range on the (-1,+1)range. Mathematically speaking, this is equivalent to tex.r * 2 -1.

Extrusion maps are perfect to zombify characters by shrinking the skin in order to highlight the shape of the bones underneath. The following image shows how a “healthy” soldier can be transformed into a corpse using a shader and an extrusion map. Compared to the previous example, you can notice how the clothing is unaffected. The shader used in the following image also darkens the extruded regions in order to give an even more emaciated look to the soldier:

 Unity 5.x Shaders and Effects Cookbook - Second Edition

Implementing a snow shader

The simulation of snow has always been a challenge in games. The vast majority of games simply baked snow directly in the models textures so that their tops look white. However, what if one of these objects starts rotating? Snow is not just a lick of paint on a surface; it is a proper accumulation of material and it should be treated as so. This recipe will show how to give a snowy look to your models using just a shader.

This effect is achieved in two steps. First, a white colour is used for all the triangles facing the sky. Second, their vertices are extruded to simulate the effect of snow accumulation. You can see the result in the following image:

 Unity 5.x Shaders and Effects Cookbook - Second Edition

Keep in mind that this recipe does not aim to create photorealistic snow effect. It provides a good starting point;however, it is up to an artist to create the right textures and find the right parameters to make it fit your game.

Getting ready

This effect is purely based on shaders. We will need to do the following:

  1. Create a new shader for the snow effect.
  2. Create a new material for the shader.
  3. Assign the newly created material to the object that you want to be snowy.

How to do it…

To create a snowy effect, open your shader and make the following changes:

  1. Replace the properties of the shader with the following ones:
    _MainColor("Main Color", Color) = (1.0,1.0,1.0,1.0)
    _MainTex("Base (RGB)", 2D) = "white" {}
    _Bump("Bump", 2D) = "bump" {}
    _Snow("Level of snow", Range(1, -1)) = 1
    _SnowColor("Color of snow", Color) = (1.0,1.0,1.0,1.0)
    _SnowDirection("Direction of snow", Vector) = (0,1,0)
    _SnowDepth("Depth of snow", Range(0,1)) = 0
    
  2. Complete them with their relative variables, as follows:
    sampler2D _MainTex;
    sampler2D _Bump;
    float _Snow;
    float4 _SnowColor;
    float4 _MainColor;
    float4 _SnowDirection;
    float _SnowDepth;
    
  3. Replace the Input structure with the following:
    struct Input {
    float2 uv_MainTex;
    float2 uv_Bump;
    float3 worldNormal;
    INTERNAL_DATA
    };
    
  4. Replace the surface function with the following one. It will color the snowy parts of the model white:
    void surf(Input IN, inout SurfaceOutputStandard o) {
    half4 c = tex2D(_MainTex, IN.uv_MainTex);
    o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));
    
    if (dot(WorldNormalVector(IN, o.Normal), 
    _SnowDirection.xyz) >= _Snow)
    o.Albedo = _SnowColor.rgb;
    else
    o.Albedo = c.rgb * _MainColor;
    o.Alpha = 1;
    }
    
  5. Configure the pragma directive so that it uses a vertex modifiers, as follows:
    #pragma surface surf Standard vertex:vert
  6. Add the following vertex modifiers that extrudes the vertices covered in snow, as follows:
    void vert(inout appdata_full v) {
    float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
    if (dot(v.normal, sn.xyz) >= _Snow)
    v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * 
    _Snow;
    }
    

You can now use the Inspectormaterial to select how much of your mode is going to be covered and how thick the snow should be.

How it works…

This shader works in two steps.

Coloring the surface

The first one alters the color of the triangles thatare facing the sky. It affects all the triangles with a normal direction similar to _SnowDirection. Comparing unit vectors can be done using the dot product. When two vectors are orthogonal, their dot product is zero; it is one (or minus one) when they are parallel to each other. The _Snowproperty is used to decide how aligned they should be in order to be considered facing the sky.

If you look closely at the surface function, you can see that we are not directly dotting the normal and the snow direction. This is because they are usually defined in a different space. The snow direction is expressed in world coordinates, while the object normals are usually relative to the model itself. If we rotate the model, its normals will not change, which is not what we want. To fix this, we need to convert the normals from their object coordinates to world coordinates. This is done with the WorldNormalVector()function, as follows:

if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >= 
_Snow)
o.Albedo = _SnowColor.rgb;
else
o.Albedo = c.rgb * _MainColor;

This shader simply colors the model white; a more advanced one should initialize the SurfaceOutputStandard structure with textures and parameters from a realistic snow material.

Altering the geometry

The second effect of this shader alters the geometry to simulate the accumulation of snow. Firstly, we identify the triangles that have been coloured white by testing the same condition used in the surface function. This time, unfortunately, we cannot rely on WorldNormalVector()asthe SurfaceOutputStandard structure is not yet initialized in the vertex modifier. We will use this other method instead, which converts _SnowDirection in objectcoordinates, as follows:

float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

Then, we can extrude the geometry to simulate the accumulation of snow, as shown in the following:

if (dot(v.normal, sn.xyz) >= _Snow)
v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

Once again, this is a very basic effect. One could use a texture map to control the accumulation of snow more precisely or to give it a peculiar, uneven look.

See also

If you need high quality snow effects and props for your game, you can also check the following resources in the Asset Storeof Unity:

  • Winter Suite ($30): A much more sophisticated version of the snow shader presented in this recipe can be found at: https://www.assetstore.unity3d.com/en/#!/content/13927
  • Winter Pack ($60): A very realistic set of props and materials for snowy environments are found at: https://www.assetstore.unity3d.com/en/#!/content/13316

Implementing a volumetric explosion

The art of game development is a clever trade-off between realism and efficiency. This is particularly true for explosions; they are at the heart of many games, yet the physics behind them is often beyond the computational power of modern machines. Explosions are essentially nothing more than hot balls of gas; hence, the only way to correctly simulate them is by integrating a fluid simulation in your game. As you can imagine, this is infeasible for runtime applications and many games simply simulate them with particles. When an object explodes, it is common to simply instantiate many fire, smoke, and debris particles that can have believableresulttogether. This approach, unfortunately, is not very realistic and is easy to spot. There is an intermediate technique that can be used to achieve a much more realistic effect: the volumetric explosions. The idea behind this concept is that the explosions are not treated like a bunch of particlesanymore; they are evolving three-dimensional objects and not just flat two-dimensionaltextures.

Getting ready

Start this recipe with the following steps:

  1. Create a new shader for this effect.
  2. Create a new material to host the shader.
  3. Attach the material to a sphere. You can create one directly from the editor bynavigating to GameObject | 3D Object | Sphere.

    This recipe works well with the standard Unity Sphere;however, if you need big explosions, you might need to use a more high-poly sphere. In fact, a vertex function can only modify the vertices of a mesh. All the other points will be interpolated using the positions of the nearby vertices. Fewer vertices mean lower resolution for your explosions.

  4. For this recipe, you will also need a ramp texture that has, in a gradient, all the colors that your explosions will have. You can create the following texture using GIMP or Photoshop. The following is the one used for this recipe:

    Unity 5.x Shaders and Effects Cookbook - Second Edition

  5. Once you have the picture, import it to Unity. Then, from its Inspector, make sure the Filter Mode is set to Bilinear and the Wrap Mode to Clamp. These two settings make sure that the ramp texture is sampled smoothly.
  6. Lastly, you will need a noisy texture. You can find many of them on the Internet as freely available noise textures. The most commonly used ones are generated using Perlin noise.

How to do it…

This effect works in two steps: a vertex function to change the geometry and a surface function to give it the right color. The steps are as follows:

  1. Add the following properties for the shader:
    _RampTex("Color Ramp", 2D) = "white" {}
    _RampOffset("Ramp offset", Range(-0.5,0.5))= 0
    
    _NoiseTex("Noise tex", 2D) = "gray" {}
    _Period("Period", Range(0,1)) = 0.5
    
    _Amount("_Amount", Range(0, 1.0)) = 0.1
    _ClipRange("ClipRange", Range(0,1)) = 1
    
  2. Add their relative variables so that the Cg code of the shader can actually access them, as follows:
    _RampTex("Color Ramp", 2D) = "white" {}
    _RampOffset("Ramp offset", Range(-0.5,0.5))= 0
    
    _NoiseTex("Noise tex", 2D) = "gray" {}
    _Period("Period", Range(0,1)) = 0.5
    
    _Amount("_Amount", Range(0, 1.0)) = 0.1
    _ClipRange("ClipRange", Range(0,1)) = 1
    
  3. Change the Input structure so that it receives the UV data of the ramp texture, as shown in the following:
    struct Input {
    float2 uv_NoiseTex;
    };
    
  4. Add the following vertex function:
    void vert(inout appdata_full v) {
    float3 disp = tex2Dlod(_NoiseTex, 
    float4(v.texcoord.xy,0,0));
    float time = sin(_Time[3] *_Period + disp.r*10);
    v.vertex.xyz += v.normal * disp.r * _Amount * time;
    }
    
  5. Add the following surface function:
    void surf(Input IN, inout SurfaceOutput o) {
    float3 noise = tex2D(_NoiseTex, IN.uv_NoiseTex);
    float n = saturate(noise.r + _RampOffset);
    clip(_ClipRange - n);
    half4 c = tex2D(_RampTex, float2(n,0.5));
    o.Albedo = c.rgb;
    o.Emission = c.rgb*c.a;
    }
    
  6. We will specify the vertex function in the pragma directive, adding the nolightmapparameter to prevent Unity from adding realistic lightings to our explosion, as follows:
    #pragma surface surf Lambert vertex:vert nolightmap
  7. The last step is to select the material and attaching the two textures in the relative slotsfrom its inspector. This is an animated material, meaning that it evolves over time. You can watch the material changing in the editor by clicking on Animated Materials from the Scene window:

    Unity 5.x Shaders and Effects Cookbook - Second Edition

How it works

If you are reading this recipe, you are already familiar with how surface shaders and vertex modifiers work. The main idea behind this effect is to alter the geometry of the sphere in a seemingly chaotic way, exactly like it happens in a real explosion. The following image shows how such explosion will look in the editor. You can see that the original mesh has been heavily deformed in the following image:

Unity 5.x Shaders and Effects Cookbook - Second Edition

The vertex function is a variant of the technique called normal extrusion. The difference here is that the amount of the extrusion is determined by both the time and the noise texture.

When you need a random number in Unity, you can rely on the Random.Range()function. There is no standard way to get random numbers within a shader, therefore,the easiest way is to sample a noise texture.

There is no standard way to do this, therefore, take the following only as an example:

float time = sin(_Time[3] *_Period + disp.r*10);

The built-in _Time[3]variable is used to get the current time from the shader and the red channel of the disp.rnoise texture is used to make sure that each vertex moves independently. The sin()function makes the vertices go up and down, simulating the chaotic behavior of an explosion. Then, the normal extrusion takes place as shown in the following:

v.vertex.xyz += v.normal * disp.r * _Amount * time;

You should play with these numbers and variables until you find a pattern of movement that you are happy with.

The last part of the effect is achieved by the surface function. Here, the noise texture is used to sample a random color from the ramp texture. However, there are two more aspects that are worth noticing. The first one is the introduction of _RampOffset. Its usage forces the explosion to sample colors from the left or right side of the texture. With positive values, the surface of the explosion tends to show more grey tones— which is exactly what happens when it is dissolving. You can use _RampOffset to determine how much fire or smoke should be there in your explosion. The second aspect introduced in the surface function is the use of clip(). Theclip()function clips (removes) pixels from the rendering pipeline. When invoked with a negative value, the current pixel is not drawn. This effect is controlled by _ClipRange, which determines the pixels of the volumetric explosions that are going to be transparent.

By controlling both _RampOffset and _ClipRange, you have full control to determine how the explosion behaves and dissolves.

There’s more…

The shader presented in this recipe makes a sphere look like an explosion. If you really want to use it, you should couple it with some scripts in order to get the most out of it. The best thing to do is to create an explosion object and turn it to a prefab so that you can reuse it every time you need. You can do this by dragging the sphere back in the Project window. Once it is done, you can create as many explosions as you want using the Instantiate() function.

However,it is worth noticing that all the objects with the same material share the same look. If you have multiple explosions at the same time, they should not use the same material. When you are instantiating a new explosion, you should also duplicate its material. You can do this easily with the following piece of code:

GameObject explosion = Instantiate(explosionPrefab) as GameObject;
Renderer renderer = explosion.GetComponent<Renderer>();
Material material = new Material(renderer.sharedMaterial);
renderer.material = material;

Lastly, if you are going to use this shader in a realistic way, you should attach a script to it, which changes its size—_RampOffsetor_ClipRange—accordingly to the type of explosion you want to recreate.

See also

A lot more can be done to make explosions realistic. The approach presented in this recipe only creates an empty shell; the explosion in it is actually empty. An easy trick to improve it is to create particles in it. However, you can only go so far with this. The short movie,The Butterfly Effect (http://unity3d.com/pages/butterfly), created by Unity Technologies in collaboration with Passion Pictures and Nvidia, is the perfect example. It is based on the same concept of altering the geometry of a sphere;however, it renders it with a technique called volume ray casting. In a nutshell, it renders the geometry as if it’s complete. You can see the following image as an example:

 Unity 5.x Shaders and Effects Cookbook - Second Edition

If you are looking for high quality explosions, refer toPyro Technix (https://www.assetstore.unity3d.com/en/#!/content/16925) on the Asset Store. It includes volumetric explosions and couples them with realistic shockwaves.

Summary

In this article, we saw the recipes to extrude models and implement a snow shader and volumetric explosion.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here