Unity Shader Chapter 7

《Unity Shader 入门精要》Chapter 7 — Reading Note

Basic Texture

Texture Mapping: Map a texture to the surface of the model.

  • Texel: Unit of the texture (like pixel of the screen)
  • Texture-mapping Coordinates: Define the corresponding 2d position of the vertex in a texture. Using a 2d variables (u, v) to represent. So it’s also called uv coordinates.

Single Texture

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
Shader "Unity Shaders Book/Chapter 7/Single Texture"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
// Main Texture
_MainTex("Main Tex", 2D) = "white" {}
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}

SubShader{
Pass{
Tags {"LightMode" = "ForwardBase"}

CGPROGRAM

#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Color;
// Variable for the main texture
sampler2D _MainTex;
// Variable for scale and translation of the main texture
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;

struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
// The first group of texture coordinates
float4 texcoord : TEXCOORD0;
};

struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
// UV coordinates after scale and translation
float2 uv : TEXCOORD2;
};

v2f vert(a2v v){
v2f o;

o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal).xyz;
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

// Transform vertex texture coordinates to final uv coordinates (Use a macro is more convenient)
// o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

return o;
}

fixed4 frag(v2f i) : SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

// Use the texture to sample the diffuse color
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

return fixed4(ambient + diffuse + specular, 1.0);
}

ENDCG
}
}

Fallback "Specular"
}

Some Texture Properties

  • Texture Type: Use this to define what your Texture is to be used for. Unity will pass the right type of texture to Unity Shader, sometimes also do some optimization.
  • Alpha Source: How is the alpha generated from the import texture.
  • Warp Mode:
    • Repeat: e.g. 1.1 -> 1.1 - 1 = 0.1
    • Clamp: e.g. 1.1 -> 1
  • Filter Mode: Filtering mode of the texture, filtered when texture produces stretch due to transformation.
    • Point: Nearest neighbor filter, or no filter. Sample only one pixel point. Result in pixel style.
    • Bilinear: Linear filter. Sample four pixel points for each target point. Result in blur.
    • Trilinear: Almost same as bilinear, but blend with mipmapping. Better effect than bilinear.
  • Max Size: Max texture size. When the size of imported texture exceeds the setting size, Unity will scale the texture to match the setting. If width/height is not power of 2, this will occupy more memory space and therefore cause performance problem. So it’s better to use texture whose width/height is power of 2.
  • Format: Format to store the texture. Higher the accuracy is, better effect we will get with higher occupied memory space.

Bump Mapping

Bump mapping uses a texture to change the normals of model’s surfaces, in order to provide more details for the model. It won’t change the positions of vertex, instead makes the model look eneven(bump).

There is two approaches.

  1. Use a height map to simulate the displacement, then get a modified normal. This is also called height mapping.
  2. Use a normal map to directly store the surface normal. This is called normal mapping.

Height Map

Height map stores the intensity, which represents the height of model’s surface. The lighter the color is, the higher the surface is.

The advantage of height map is from it we can clearly know the bump degree of the model’s surface. However, we can’t directly retrieve the normal. We need to calculate the value by grayscale, which consumes more performance. Therefore, usually use normal mapping to modify light, while using height map to provide some other light’s information.

Normal Map

Normal map stores the normal direction of the surface, whose components are in range [-1, 1]. For a pixel, its components are from 0 to 1. Therefore we need a mapping:

pixel = (normal - 1) / 2

Direction is relative to coordinate space. For a normal of a vertex, it’s defined in object space, which is called object-space normal map.

However, in practice, we will another space called tangent space. Its origin is the vertex itself, z axis is the normal direction(n), x axis is the tangent direction(t), and y axis is obtained by the cross product of n and t. This kind of texture is called tangent-space normal map.

Comparison

  • Object-space
    • Easy and clear.
    • Smooth boundary.
  • Tangent-space
    • High freedom.
    • Ability to uv animation.
    • Reusable.
    • Compressible.

Code (In Tangent Space)

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
Shader "Unity Shaders Book/Chapter 7/Normal Map In Tangent Space"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
_MainTex("Main Tex", 2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Float) = 1.0
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}

SubShader{
Pass{
Tags{"LightMode" = "ForwardBase"}

CGPROGRAM

#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;

struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};

struct v2f{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};

v2f vert(a2v v){
v2f o;

o.pos = UnityObjectToClipPos(v.vertex);

// _MainTex and _BumpTex use the same uv coordinate to reduce the number of register
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

// Compute the binormal
// float3 binormal = cross(normalize(v.normal), normalize(v.tangent)) * v.tangent.w;
// Construct tangent rotation matrix (transform from object space to tangent space)
// float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal.xyz);
// Or Just use the built-in function
// We don't need to explicitly define rotation
TANGENT_SPACE_ROTATION;

// Calculate light and view direction in tangent space
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

return o;
}

fixed4 frag(v2f i) : SV_Target{
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);

// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// Texture should be marked as "Normal map", then use the built-in function
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular * pow(saturate(dot(tangentNormal, halfDir)), _Gloss);

return fixed4(ambient + diffuse + specular, 1.0);
}

ENDCG
}
}
}

Different Bump Scale

Normal Map Type

When we mark the texture as an normal map, we can use the built-in function UnpackNormal to retrieve the correct normal. In different platform, Unity will compress the texture in different ways.

If we import a height map, we can set Create from Grayscale check box to generate a normal map from it. Then we can treat this texture as a normal map.

Ramp Texture

Except defining the color of an object, we can also use texture to store any surface properties. One common usage is using ramp texture to control the result of diffuse lit.

In ramp shading, the idea is to modulate the diffuse coefficient by a 1D lookup texture. Depending on what you put in this texture you can generate different effects.

1
2
3
4
5
// Use the texture to sample the diffuse color
fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;

fixed3 diffuse = _LightColor0.rgb * diffuseColor;

Mask Texture

Mask texture allows us to protect some regions from being modulated.

The basic idea is to sample the texel from the mask texture, then multiply one of its channel’s value with one of the surface’s property.

1
2
3
4
// Get the mask value
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
// Computer the specular
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss) * specularMask;

In practice, we can use mask no only to protect some regions, but also control any proporties we want. For example, we use all the channels (RGBA) of a texture, the intensity of specular in R channel, the intensity of edge lighting in G channel, gloss of specular in B channel and intensity of emissive in A channel.