Unity 渲染原理(十一)Unity基础纹理(渐变纹理和遮罩纹理)

继续上一篇博客,继续讨论下基础纹理,这次我们讨论下渐变纹理和遮罩纹理

渐变纹理

在一开始,我们在渲染中使用纹理是为了定义一个物体的颜色,但后来人们发现,纹理其实可以用来存储任何表面属性,其中一种常见的用法就是使用渐变纹理来控制漫反射光照的结果。之前计算漫反射光照时,我们都是用表面法线和光照方向的点积结果与材质的反射率相乘来得到表面的漫反射光照,但有时候,我们希望能够更加灵活地控制光照结果,此时我们就可以使用一张渐变纹理来控制。

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;
};

v2f vert (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"
}

在上面的代码中,我们是用了半兰伯特的光照模型,通过对法线方向和光照方向点积做一次0.5倍的缩放以及一个0.5大小的偏移来计算半兰伯特部分的halfLambert。这样,我们得到的halfLambert的范围被映射到了[0,1]之间。之后我们是用halfLambert来构建一个纹理坐标,并用这个纹理坐标对_RampTex进行采样。

由于_RampTex实际上就是一个一维纹理(它在纵轴方向上颜色不变),因此纹理坐标的u和v方向我们都使用halfLambert。然后把从渐变纹理采样得到的颜色和材质颜色_Color相乘,得到最终的漫发射颜色。

剩下的代码就是计算高光反射和环境光,并把它们的结果进行相加。

需要注意的是,我们需要将渐变纹理的Wrap Mode模式设置为Clamp,防止对纹理进行采样时由于浮点数精度而造成的问题。

如果使用Repeat模式,在高光区域会出现一些黑点。这是由于浮点数精度造成的。当我们使用fixed2(halfLambert, halfLambert)对渐变纹理进行采样时,虽然理论上halfLambert会在[0,1]之间,但可能会出现1.00000001这样的值,如果我们使用的是Repeat模式,那么就会舍弃整数部分,只保留小数部分,采样的坐标就是0.00000001了

遮罩纹理

遮罩纹理非常有用,在很多商业游戏中都可以见到它的身影,那么什么事遮罩呢?

简单来讲,遮罩允许我们保护某些区域,使它们免于某些修改。例如,在之前的实现中,我们都是吧高光反射应用到模型表面的所有地方,即所有的像素使用相同大小的高光强度和高光指数。但有时候,我们希望模型表面某些区域的反射光强烈一些,而某些区域弱一些。

为了得到更加细腻的效果,我们就可以使用一张遮罩纹理来控制光照。另一种常见的应用是在制作地形材质时需要混合多张图片,例如表现草地的纹理,表现石子的纹理,表现裸露土地的纹理等,使用遮罩纹理可以控制如何混合这些纹理。

使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中的某个(或某几个)通道值来与某种表面属性相乘,这样当该通道值为0时,可以保护表面不受该属性的影响。

总而言之,使用遮罩纹理可以让美术人员更加精准(像素级别)地控制模型表面的各种属性。

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;
};

v2f vert (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"
}

这里的代码和之前切线空间下对法线纹理进行采样时使用的代码完全一样,只是在计算高光反射的时候,首先对遮罩纹理进行采样。由于我们是用的遮罩纹理的每个纹素的rgb分量其实都完全相同,我们只是采用了r通道来计算掩码值,但是浪费了其他的空间。在实际游戏制作中,我们往往会充分利用遮罩纹理中的每一个颜色通道来存储不同的表面属性。