Unity Rendering Principle (11) Unity Basic Textures (gradual change textures and mask textures)

Continuing with the previous blog, let’s discuss basic textures. This time we will discuss gradual change textures and mask textures

Gradual change of texture

In the beginning, we used textures in rendering to define the color of an object, but later it was discovered that textures can actually be used to store any surface property, and one common usage is to use gradual change textures to control diffuse lighting results. When calculating diffuse illumination before, we used the dot product of the surface normal and illumination direction to multiply the reflectivity of the material to get the diffuse illumination of the surface, but sometimes we want to be able to control the illumination results more flexibly, At this time, we can use a gradual change texture to control.

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
Shader "Unlit/RampTexture"
{
Properties
{
_Color("Color Tint", Color) = (1,1,1,1)
_RampTex ("Ramp Tex", 2D) = "white" {}
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
float _Gloss;

struct appdata
{
float4 vertex : POSITION;
float3 normal: NORMAL;
float4 texcoord: TEXCOORD0;
};

struct v2f
{
float4 pos: SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos: TEXCOORD1;
float2 uv : TEXCOORD2;
};

green v2f (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
return o;
}

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

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;

fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;

fixed3 diffuse = _LightColor0.rgb * diffuseColor;

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

fixed3 halfDir = normalize(worldLightDir + viewDir);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

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

Fallback "Specular"
}

In the above code, we use the half-Lambert illumination model to calculate the half-Lambert of the half-Lambert part by doing a 0.5x scaling on the dot product of normal and illumination directions and an offset of 0.5 size. In this way, the range of halfLambert we get is mapped between [0,1]. We then use halfLambert to construct a texture coordinate and use this texture coordinate to sample the _RampTex.

Since _RampTex is actually a one-dimensional texture (it doesn’t change color on the vertical axis), we use halfLambert for both the u and v directions of the texture coordinates. Then multiply the color sampled from the gradual change texture with the material color _Color to get the final diffuse emission color.

The rest of the code is to calculate the specular reflection and ambient light, and add their results.

It should be noted that we need to set the Wrap Mode of the gradual change texture to Clamp to prevent problems caused by floating point precision when sampling the texture.

If you use Repeat mode, there will be some black spots in the highlight area. This is due to floating-point precision. When we use fixed2 (halfLambert, halfLambert) to sample the gradual change texture, although in theory halfLambert will be between [0,1], it may have a value like 1.00000001. If we use Repeat mode, then the integer part will be discarded, only the fractional part will be kept, and the sampled coordinate will be 0.00000001

Mask texture

The mask texture is very useful, and it can be seen in many commercial games, so what is the mask?

Simply put, masking allows us to protect certain areas from certain modifications. For example, in the previous implementation, we applied highlight reflection to all parts of the model surface, that is, all pixels use the same size highlight intensity and highlight index. But sometimes, we want the reflected light on some areas of the model surface to be stronger and some areas to be weaker.

To get a more detailed effect, we can use a mask texture to control the lighting. Another common application is to mix multiple images when making terrain materials, such as grass textures, gravel textures, bare earth textures, etc. Using mask textures can control how these textures are mixed.

The general process of using mask textures is to obtain the texel value of the mask texture by sampling, and then use one (or several) of the channel values to multiply with a certain surface property, so that when the channel value is 0, the surface can be protected from this property.

All in all, using mask textures allows artists to control various properties of the model surface with more precision (pixel level).

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
Shader "Unlit/MaskTexture"
{
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
_SpecularMask("Specular Mask", 2D) = "white" {}
_SpecularScale("Specular Scale", Float) = 1.0
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

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

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

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

green v2f (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

TANGENT_SPACE_ROTATION;
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);

fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
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 specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;

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

Fallback "Specular"
}

The code here is exactly the same as the code used when sampling normal textures in tangent space before, except that when calculating the specular reflection, the mask texture is sampled first. Since the rgb component of each texel of the mask texture we are using is actually exactly the same, we just use the r channel to calculate the mask value, but waste other space. In actual game production, we tend to make full use of each color channel in the mask texture to store different surface properties.