Unity Shader入门精要读书笔记系列
第1章 欢迎来到Shader的世界
第2章 渲染流水线
第3章 Unity Shader基础
第4章 学习Shader所需的数学基础
第5章 开始 Unity Shader 学习之旅
第6章 Unity中的基础光照
第7章 基础纹理
文章目录
前言
上一章中我们学习了基础光照模型,仅仅通过光照模型得到的漫反射颜色往往很单调。
因此我们通常会使用一张纹理来代替物体的漫反射颜色。
不过纹理的作用远不止这个,让我们通过学习本章来了解Unity中纹理的作用。
一、初识纹理
纹理映射技术(texture mapping)
逐纹素(texel)的控制模型的颜色,实现把一张图“黏”在模型表面。
纹理映射坐标(texture-mapping coordinates)
建模时用纹理展开技术把纹理映射坐标(UV坐标)存储在每个顶点上,用一个二维变量(u,v)
表示。u,v通常被规划到[0,1]。
1.单张纹理采样实践
Shader "Unity Shaders Book/Chapter 7/SingleTexture"
{
Properties
{
// 漫反射颜色
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_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
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
//在 Unity 中,我们需要使用 纹理名_ST 的方式来声明某个纹理的属性。其中, ST 是缩放 (scale 和平移 (translation 的缩写。
//_MainTex_ST可以让我们得到该纹理的缩放和平移 (偏移)值 _MainTex_ST.xy 存储的是缩放值,而_MainTex_ST.zw 存储的是偏移值。
// 这些值可以在材质面板的纹理属性中调节
float4 _MainTex_ST;
float4 _Specular;
float _Gloss;
struct a2v
{
// 模型空间的顶点坐标
float4 vertex : POSITION;
// 模型空间的法线方向
float3 normal : NORMAL;
// 第一组纹理坐标存储到该变量中
float3 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;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // UnityCG.cginc内置宏 参数是 顶点纹理坐标 和 纹理名
return o;
}
// 在计算漫反射时使用纹理中的纹素值
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 材质的反射率(漫反射系数) 使用CG的 tex2D 函数对纹理进行采样
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.纹理的属性
纹理格式(Texture Type)
纹理形状(Texture Shape)
Alpha Source
input texture Alpha:该纹理自带透明度
from gray Scale :依据灰度产生Alpha通道
Non Power of 2
Unity会对宽高均为2的整数幂的纹理压缩为RGB Compressed DXT1格式,减少内存占用。
当宽高不为2的整数幂是,根据选项Unity自动修改为对应宽高均为2的整数幂的纹理。
Read/Write Enabled
是否允许运行时用代码修改这张纹理的颜色。
Generate Mip Maps
是否生成多级渐远纹理。
为什么会增加到1.3倍内存,可以去看闫神的Games101有讲解。
Wrap Mode
决定了当纹理坐标超过[0,1]范围后将会如何被平铺
Repeat:超过1的整数部分会被舍弃。
Clamp:截取边界值(0或1)。
左边Repeat,右边Clamp。
Filter Mode
决定纹理由于变换而产生拉伸时将会采用哪种滤波模式。会影响放大或缩小纹理时得到的图片质量。
滤波效果 Point<Bilinear<Trilinear,性能消耗也依次提升。一般使用 Bilinear 双线性插值。
原理可以去看闫神的Games101。
Aniso Level
各向异性级别。以一个过小的角度观察纹理时,此数值越高观察到的纹理质量就越高。一般用于3D游戏或有视角缩放功能的游戏中。当从一个陡峭的角度看纹理时,增加纹理质量。适合地面和地面纹理。
二、凹凸映射(bump mapping)
使用一张纹理来修改模型表面的法线,使模型展现出凹凸不平的细节。(并不是真的修改模型顶点位置)
一般有高度映射,和法线映射两种实现。
1.高度映射(height mapping)
使用一张高度纹理(height map)来模拟表面位移(displacement),得到一个修改后的法线值。
高度图是一张灰度图,其 Alpha Source 需要使用 from gray Scale :依据灰度产生Alpha通道。
2.法线映射(normal mapping)
使用一张法线纹理直接存储表面法线。
不同空间下的法线纹理
对于模型顶点自带的法线,将修改后的法线存储在一张纹理中。
这种纹理称为模型空间的法线纹理(object-space normal map)。
然而在实际制作中,我们往往使用模型顶点的切线空间(tangent space)来存储法线。
这种纹理称为切线空间的法线纹理(tangent-space normal map)。
模型空间下的法线纹理看起来是“五颜六色”的。这是因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,而每个点存储的法线方向是各异的,有的是(0,1, 0), 经过映射后存储到纹理中就对应了RGB(0.5, 1, 0.5) 浅绿色,有的是(0, -1, 0), 经过映射后存储到纹理中就对应了 0.5, 0, 0.5)紫色。
而切线空间下的法线纹理看起来几乎全部是浅蓝色的。
这是因为,每个法线方向所在的坐标空间是不一样的,即是表面每点各自的切线空间。这种法线
纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向。也就是说,如果一个点的法线
方向不变,那么在它的切线空间中 ,新的法线方向就是z轴方向,即值为(0,0, 1), 经过映射后存
储在纹理中就对应了 RGB(0.5, 0.5, 1) 浅蓝色 。而这个颜色就是法线纹理中大片的蓝色。这些蓝色
实际上说明顶点的大部分法线是和模型本身法线一样 的,不需要改变。
各自的优点
模型空间下:
1)实现简单 ,更加直观。我们甚至都不需要模型原始的法线和切线等信息 ,也就是说,计算更少。生成它也非常简单 ,而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV 方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的。
2)在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。
切线空间下:
1)自由度很高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。
2)可进行 UV 动画。比如,我们可以移动 个纹理的 UV 坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。原因同上。这种 UV 动画在水或者火山熔岩这种类型的物体上会经常用到。
3)可以重用法线纹理。比如,一个砖块,我们仅使用一张法线纹理就可以用到所有的6个面上。
4)可压缩。由于切线空间下的法线纹理中法线的Z方向总是正方向,因此我们可以仅存储XY 方向,而推导得到Z方向。而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储每个方向的值,不可压缩。
3.实践
由于切线空间下的法线纹理优点更多,所有我们一般使用切线空间下的法线纹理计算光照模型。
计算光照模型需要统一坐标系,一般在切线空间或者世界空间下计算。
在切线空间下计算
Shader "Unity Shaders Book/Chapter 7/NormalMapTangentSpace"
{
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
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _Specular;
float _Gloss;
struct a2v
{
// 模型空间的顶点坐标
float4 vertex : POSITION;
// 模型空间的法线方向
float3 normal : NORMAL;
// 模型空间的顶点切线方向、 为什么是 float4 ? 需要使用w分量来决定副切线的方向性
float4 tangent : TANGENT;
// 第一组纹理坐标存储到该变量中
float3 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;
// 计算副切线
float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
// 计算转换矩阵
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); // 等同于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;
// If the texture is not marked as "Normal map"
//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); // 平方根运算
// 如果贴图是法线贴图 "Normal map", 可以使用内置方法
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 材质的反射率(漫反射系数) 使用CG的 tex2D 函数对纹理进行采样
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"
}
在世界空间下计算
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unity Shaders Book/Chapter 7/NormalMapWorldSpace"
{
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
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _Specular;
float _Gloss;
struct a2v
{
// 模型空间的顶点坐标
float4 vertex : POSITION;
// 模型空间的法线方向
float3 normal : NORMAL;
// 模型空间的顶点切线方向、 为什么是 float4 ? 需要使用w分量来决定副切线的方向性
float4 tangent : TANGENT;
// 第一组纹理坐标存储到该变量中
float3 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
// 计算世界空间下的顶点切线、副切线和法线的矢量表示
// 把它们按列摆放得到:从切线空间到世界空间的变换矩阵
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;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x , worldNormal.x , worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y , worldNormal.y , worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z , worldNormal.z , worldPos.z);
return o;
}
// 在世界空间下进行光照计算
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// 世界空间下的光照和视角方向
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 获取切线空间下的法线
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
//利用TtoW0~2存储的变换矩阵将法线变换到世界空间下
bump = normalize(half3(dot(i.TtoW0.xyz, bump),dot(i.TtoW1.xyz, bump),dot(i.TtoW2.xyz, bump)));
// 材质的反射率(漫反射系数) 使用CG的 tex2D 函数对纹理进行采样
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
// 环境光部分
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
// 漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
// 高光反射
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
注意:只有设置成 Normal Map的纹理才能使用 UnpackNormal 函数来采样法线方向。
同时Unity可以根据不同平台对法线纹理进行压缩(例如使用DXT5nm格式)
效果图对比
三、渐变纹理(Ramp Texture)
Ramp Texture可用于卡通渲染,fake BRDF,特效等等…应用范围非常广。
Shader "Unity Shaders Book/Chapter 7/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
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
float4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float3 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;
//利用halfLambert来构建一个纹理坐标,并用该纹理坐标对渐变纹理_RampTex进行采样
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"
}
分别使用三张不同的Ramp Texture实现的效果如下。
需要注意的点是,Ramp Texture的Wrap Mode格式一定要选为Clamp模式,否则当我们使用 fixed2(haIfLam bert,
halfLambert)对渐变纹理进行采样时,虽然理论上 halfLambert 值在[O, 1] 之间。但由于浮点数误差,会有 1.00000001 这样的值出现。如果使用的是 Repeat 模式,此时就会舍弃整数部分,只保留小数部分0.0000000001,得到的值就会对应渐变图中最左边的值。
四、遮罩纹理(mask texture)
通过控制遮罩纹理的纹素值,修改其中某个或某几个通道的值为我们想要的值(例如texel.r = 0)。这样可以使美术人员更加在精准(像素级别)地控制模型表面的各种性质。
实践
使用高光遮罩纹理,逐像素地控制模型表面的高光反射强度。
这里只使用四个通道中的 r 通道。
Shader "Unity Shaders Book/Chapter 7/MaskTexture"
{
Properties
{
_Color("Color Tint", Color) = (1,1,1,1)
_MainTex("Main Tex", 2D) = "white"{}
_BumpMap("Bump 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
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.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;
float3 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; //对SpecularMask进行采样,r分量来计算掩码值,在把两个相乘
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通道。
在游戏《D0TA2 》的开发中,开发人员为每个模型使用了4张纹理:一张用于定义模型颜色,一张用于定义表面法线,另外两张则都是遮罩纹理。这样,两张遮罩纹理提供了共8种额外的面属性,这使得游戏中的人物材质自由度很强,可以支持很多高级的模型属性。读者可以在他们的官网上找到关于《D0TA2》的更加详细的制作资料,包括游戏中的人物模型、纹理以及制作手册等。这是非常好的学习资料。具体怎么学习,可以参考B站庄懂老师的TA课程,里面有一节课专门讲解到。