这一节我们将学习纹理映射的实现方式,包括单张图片纹理,以及在游戏中广泛应用的凹凸纹理、渐变纹理和遮罩纹理。
1 单张纹理 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 ) _MainTex ("Main Tex", 2 D) = "white" {} _Specular ("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss ("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Specular; float _Gloss; struct a2v { 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(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize (i.worldNormal); fixed3 worldLightDir = normalize (UnityWorldSpaceLightDir(i.worldPos)); fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max (0 , dot (worldNormal, worldLightDir)); 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" }
渲染效果如下:
2 凹凸纹理 之前学习过凹凸纹理的实现有两种方式,一种是使用高度贴图,纹理中记录顶点法线的位移(高度),但是这样做不够逼真,物体边缘处还是平滑的,很容易看出破绽;另一种是法线贴图,纹理中直接记录每个顶点的法线方向,由于法线方向的分量范围在[-1, 1],而像素的分量范围为[0, 1],因此我们需要做一个映射,通常使用的映射就是: $$ piexl = \frac{normal + 1}{2} $$ 这就要求我们对法线纹理进行纹理采样后,还要进行一个反映射,即: $$ normal = pixel \times 2 - 1 $$ 既然记录的是法线方向,那么一定有一个参考坐标系,最简单的当然是直接记录模型空间下的法线方向,这样我们可以正常转换到世界空间然后直接计算光照,这种纹理称为模型空间下的法线纹理 ;更好的方式是记录每个顶点的切线空间下的法线方向,所谓切线空间是指以顶点切线为 x 轴,副切线为 y 轴,顶点法线为 z 轴的空间,这种纹理称为切线空间下的法线纹理 。为什么存储切线空间中的纹理更好呢?我们可以对比一下两种法线纹理的优缺点:
使用模型空间的法线纹理实现简单,也更加直观。并且在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。
使用切线空间的法线纹理,首先,自由度很高,模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了,而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果;其次,可进行 UV 动画,比如,我们可以移动一个纹理的 UV 坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果,原因同上。这种 UV 动画在水或者火山熔岩这种类型的物体上会经常用到;再次,可以重用法线纹理,比如,一个砖块,我们仅使用一张法线纹理就可以用到所有的 6 个面上;最后,可压缩,由于切线空间下的法线纹理中法线的 Z 方向总是正方向,因此我们可以仅存储 XY 方向,而推导得到 Z 方向。而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储 3 个方向的值,不可压缩。
显然切线空间下的法线纹理好处更多。
接下来实现凹凸纹理的效果。我们需要在计算光照模型中统一各个方向矢量所在的坐标空间。 由于法线纹理中存储的法线是切线空间下的方向,因此我们通常有两种选择:一种选择是在切线空间下进行光照计算,此时我们需要把光照方向、视角方向变换到切线空间下;另一种选择是在世界空间下进行光照计算,此时我们需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向 进行计算。从效率上来说,第一种方法往往要优于第二种方法,因为我们可以在顶点着色器中就完成对光照方向和视角方向的变换,而第二种方法由于要先对法线纹理进行采样,所以变换过程必须在片元着色器中实现,这意味着我们需要在片元着色器中进行一次矩阵操作。但从通用性角度来说,第二种方法要优于第一种方法,因为有时我们需要在世界空间下进行一些计算,例如在使用 Cubemap 进行环境映射时,我们需要使用世界空间下的反射方向对 Cubemap 进行采样。
2.1 在切线空间下计算 我们首先来实现第一种方法,即在切线空间下计算光照模型。基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。为此,我们首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即我们需要知道从模型空间到切线空间的变换矩阵。这个变换矩阵的逆矩阵,即从切线空间到模型空间的变换矩阵是非常容易求得的,就只有坐标轴的旋转和原点的平移,而对于矢量变换,不需要平移,只有坐标轴的旋转,因此我们在顶点着色器中按切线 (x 轴)、副切线 (y 轴)、法线 (z 轴)的顺序按列 排列即可得到从切线空间到模型空间的变换矩阵。如果一个变换中仅存在平移和旋转变换,那么这个变换的逆矩阵就等于它的转置矩阵,而从切线空间到模型空间的变换正是符合这样要求的变换。因此,从模型空间到切线空间的变换矩阵就是从切线空间到模型空间的变换矩阵的转置矩阵,我们把切线 (x 轴)、副切线 (y 轴)、法线 (z 轴)的顺序按行 排列即可。CG 中矩阵填充默认刚好是按行填充。
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 96 97 98 99 100 101 102 103 104 105 106 107 Shader "Unity Shaders Book/Chapter 7 /Normal Map In Tangent Space" { Properties { _Color ("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex ("Main Tex", 2 D) = "white" {} _BumpMap ("Normal Map", 2 D) = "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 #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" 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); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; fixed3 Binormal = cross (v.normal, v.tangent.xyz) * v.tangent.w; float3x3 ObjToTangent = float3x3(v.tangent.xyz, Binormal, v.normal); o.lightDir = mul(ObjToTangent, ObjSpaceLightDir(v.vertex)); o.viewDir = mul(ObjToTangent, ObjSpaceViewDir(v.vertex)); 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; 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.rgb * pow (max (0 , dot (tangentNormal, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0 ); } ENDCG } } FallBack "Specular" }
我们可以调整材质面板中的 Bump Scale 属性控制凹凸程度,Bump Scale = -1 时渲染效果如下:
Bump Scale = 1 时渲染效果如下:
可以看出凹凸程度的正负决定了物体表面是“凸出来”还是“凹进去”。
2.2 在世界空间下计算 现在,我们在世界空间下计算光照。这时我们需要在片元着色器中把法线方向从切线空间变换到世界空间下。因此要先在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示按列排列来得到。尽管这种方法需要更多的计算,但在需要使用 Cubemap 进行环境映射等情况下,我们就需要使用这种方法。
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 96 97 98 99 100 101 102 103 104 Shader "Unity Shaders Book/Chapter 7 /Normal Map In World Space" { Properties { _Color ("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex ("Main Tex", 2 D) = "white" {} _BumpMap ("Normal Map", 2 D) = "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 #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" 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; float4 TangentToWorld0 : TEXCOORD01; float4 TangentToWorld1 : TEXCOORD02; float4 TangentToWorld2 : TEXCOORD03; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; fixed3 WorldNormal = UnityObjectToWorldNormal(v.normal); fixed3 WorldTangent = UnityObjectToWorldDir(v.tangent.xyz); fixed3 WorldBiTangent = cross (WorldNormal, WorldTangent) * v.tangent.w; float3 WorldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.TangentToWorld0 = float4(WorldTangent.x, WorldBiTangent.x, WorldNormal.x, WorldPos.x); o.TangentToWorld1 = float4(WorldTangent.y, WorldBiTangent.y, WorldNormal.y, WorldPos.y); o.TangentToWorld2 = float4(WorldTangent.z, WorldBiTangent.z, WorldNormal.z, WorldPos.z); return o; } fixed4 frag(v2f i) : SV_Target { float3 WorldPos = float3(i.TangentToWorld0.w, i.TangentToWorld1.w, i.TangentToWorld2.w); fixed3 worldLightDir = normalize (UnityWorldSpaceLightDir(WorldPos)); fixed3 viewDir = normalize (UnityWorldSpaceViewDir(WorldPos)); fixed3 halfDir = normalize (worldLightDir + 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))); float3x3 TangentToWorld = float3x3(i.TangentToWorld0.xyz, i.TangentToWorld1.xyz, i.TangentToWorld2.xyz); fixed3 WorldNormal = normalize (mul(TangentToWorld, tangentNormal)); fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max (0 , dot (WorldNormal, worldLightDir)); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow (max (0 , dot (WorldNormal, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0 ); } ENDCG } } FallBack "Specular" }
渲染效果和在切线空间下计算是完全一样的。这里需要特别说明结构体 v2f 中分别用三个向量存储矩阵,这是因为插值寄存器最多只能存储 float4 类型的变量,因此矩阵要分行从顶点着色器传递给片元着色器。从顶点着色器传递给片元着色器的过程中,插值寄存器中的值(顶点的属性)会被自动插值为片元的属性。
另外我们也可以导入高度纹理,然后在纹理面板勾选 Create from Grayscale 来自动生成切线空间的法线纹理。
3 渐变纹理 我们在图形学中学过,纹理在现代 GPU 中可以认为是一块可以支持快速查询的内存,因此不仅可以用来存储颜色,还可以用来存储任何属性,上面的法线贴图就是一个例子。另一个常见的用法就是使用渐变纹理来控制漫反射光照的结果。在之前计算漫反射光照时,我们都是使用表面法线和光照方向的点积结果与材质的反射率相乘来得到表面的漫反射光照。但有时,我们需要更加灵活地控制光照结果。
使用渐变纹理控制光照结果可以保证物体的轮廓线相比于之前使用的传统漫反射光照更加明显,而且能够提供多种色调变化,现在很多卡通风格的渲染中都使用了这种技术。
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 Shader "Unity Shaders Book/Chapter 7 /Ramp Texture" { Properties { _Color ("Color Tint", Color) = (1 , 1 , 1 , 1 ) _RampTex ("Ramp Tex", 2 D) = "white" {} _Specular ("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss ("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _RampTex; float4 _RampTex_ST; fixed4 _Specular; float _Gloss; struct a2v { 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(a2v 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" }
使用不同渐变纹理的渲染效果如下:
需要注意的是我们要把渐变纹理的 Wrap Mode 设为 Clamp 模式,以防止对纹理进行采样时由于浮点数精度而造成的问题。下图是使用 Repeat 模式的渐变纹理效果:
下图是使用 Clamp 模式的渐变纹理效果:
可以看出在 Repeat 模式下高光区域存在一些黑点,这是由浮点精度造成的。当我们使用 fixed2(haIfLambert, halfLambert) 对渐变纹理进行采样时,虽然理论上 haIfLambert 的值在 [0, 1] 之间,但是可能会有 1.00001 这样的值出现。如果使用 Repeat 模式,此时就会舍 弃整数部分,只保留小数部分,得到的值就是 0.00001, 对应了渐变图中最左边的值,即黑色。因此,就会出现图中这样在高光区域有黑点的情况。所以我们只需要把渐变纹理的 Wrap Mode 设为 Clamp 模式就可以解决这种问题。超过 1 则截取到 1,就可以取到正常值。
4 遮罩纹理 遮罩纹理 (mask texture) 是极其有用的一种纹理,在很多商业游戏中都可以见到它的身影。那么什么是遮罩呢? 简单来讲,遮罩允许我们可以保护某些区域,使它们免于某些修改。例如,在之前的实现中,我们都是把高光反射应用到模型表面的所有地方,即所有的像素都使用同样大小的高光强度和高光指数。但有时,我们希望模型表面某些区域的反光强烈一些,而某些区域弱一些。为了得到更加细腻的效果,我们就可以使用一张遮罩纹理来控制光照。另一种常见的应用是在制作地形材质时需要混合多张图片,例如表现草地的纹理、表现石子的纹理、表现裸露土地的纹理等,使用遮罩纹理可以控制如何混合这些纹理。
使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(例如 texel.r)来与某种表面属性进行相乘,这样,当该通道的值为 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 Shader "Unity Shaders Book/Chapter 7 /Mask Texture" { Properties { _Color ("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex ("Main Tex", 2 D) = "white" {} _BumpMap ("Normal Map", 2 D) = "bump" {} _BumpScale("Bump Scale", Float) = 1.0 _SpecularMask ("Specular Mask", 2 D) = "white" {} _SpecularScale ("Specular Scale", Float) = 1.0 _Specular ("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss ("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } 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 a2v { 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(a2v 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); fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv)); 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); fixed 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" }
未使用遮罩纹理的渲染效果如下:
使用高光遮罩纹理的效果如下:
可以看出,遮罩纹理可以让我们更加精细地控制光照细节,得到更细腻的效果。
在真实的游戏制作过程中,遮罩纹理已经不止限于保护某些区域使它们免于某些修改,而是可以存储任何我们希望逐像素控制的表面属性。通常,我们会充分利用一张纹理的 RGBA 四个通道,用于存储不同的属性。例如,我们可以把高光反射的强度存储在 R 通道,把边缘光照的强度存储在 G 通道,把高光反射的指数部分存储在 B 通道,最后把自发光强度存储在 A 通道。
在游戏《DOTA2》的开发中,开发人员为每个模型使用了 4 张纹理:一张用于定义模型颜色,一张用于定义表面法线,另外两张则都是遮罩纹理。这样,两张遮罩纹理提供了共 8 种额外的表面属性,这使得游戏中的人物材质自由度很强,可以支持很多高级的模型属性。