通常使用一张纹理来代替物体的漫反射颜色。使用纹理的Shader中,需要对纹理进行采样。 使用CG的tex2D(_MainTex,uv)函数进行纹理采样,第一个参数是需要被采样的纹理,第二是float2类型的纹理坐标,该坐标在顶点着色器中由_MainTex_ST对定点纹理坐标进行变换得到。
Shader "Custom/Chapter7_SingleTexture" {
Properties{
_Color("Color",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"
fixed4 _Color;
sampler2D _MainTex;
//纹理类型的属性需要再声明一个float4类型的变量,命名格式为 纹理名_ST
//其中,ST是缩放和偏移的缩写 纹理名_ST.xy存储的是缩放值,纹理名_ST.zw存储的是偏移值
//对应材质面板的纹理属性的Tiling和Offset调节项
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex:POSITION;
float3 normal:NORMAL;
//使用texcoord变量存储模型的第一组纹理坐标
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 = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
//对纹理坐标进行变换,对应材质面板的Tiling和Offset调节项
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));
//使用tex2D做纹理采样,将采样结果和颜色属性_Color相乘作为反射率
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*albedo*pow(saturate(dot(halfDir,worldNormal)), _Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
实现效果:
Warp Mode属性
每张纹理在导入到Unity后,在纹理的检视面板中有Warp Mode属性,该属性决定了当纹理坐标超过[0,1]范围后如何被平铺,有Repeat(重复平铺),Clamp(截取平铺)截取平铺是当超过1后的纹理坐标的对应的顶点颜色值均为1处的值。
凹凸映射
凹凸映射的目的是使用一张纹理来修改模型表面的法线。这种方法不会真正该改变模型顶点位置,使模型看起来具有凹凸的效果,这点从模型的轮廓可以看出来。
凹凸映射的两种方法:
- 高度纹理
使用一张高度纹理来模拟表面位移,得到一个修改后的法线值,该方法也叫“高度映射"。
高度图中存储的是强度值,用于表示模型表面的海拔高度。颜色越浅表示越向外凸起,颜色越深越向里凹。实时计算中,不能直接得到表面法线,需要由像素的灰度值计算得到,需要消耗更多性能。 - 法线纹理
使用一张法线纹理直接存储表面法线,该方法也叫“法线映射”。
法线纹理中存储的是表面的法线方向,法线矢量值范围[-1,1],纹理中的颜色值范围[0,1],因此将法线存储在一张纹理中,有一个映射过程:
pixel= ( normal+1)/2
因此在对法线纹理进行采样得到像素颜色后,为了得到对应的发现方向,需要进行反映射过程
normal=2(pixel)-1
法线方向所在的坐标空间
- 模型空间的法线纹理
模型顶点自带的法线,定义在模型空间中,若将修改后的模型空间中的表面法线存储在一张纹理中,则为模型空间中的法线纹理 - 切线空间的法线纹理
模型的每一个 顶点有自己对应的切线空间,z轴为顶点的法线方向,x轴为顶点的切线方向,y轴为顶点的副切线方向,由法线和切线的叉积得到,这种纹理被称为切线空间的法线纹理。
模型空间下的法线纹理颜色比较丰富,这是因为模型空间下各顶点的的法线所在的坐标系为同一个坐标系,而各个顶点法线方向各异,因此映射过后颜色相对比较丰富。
而切线空间下的法线纹理颜色集中在浅蓝色,这是由于各自顶点都有自己的坐标系,法线的方向尽管在同一个坐标下方向各异,但在自身的切线空间坐标系下,法线方向均指向z轴,因此映射到纹理上,像素颜色单一。
使用哪种坐标空间只是第一步,得到法线信息是为了转化到相应的坐标系(如世界空间)后进行后续的光照计算。
使用模型空间法线纹理优势:
- 直观
转化到其他坐标空间计算相对简单。 - 在纹理坐标的边缘处,可见突变较少。
模型空间下的法线纹理是在同一个坐标系下,边缘处可以通过插值得到平滑效果。
使用切线空间法线纹理优势:
- 重用性较高。
模型空间下法线纹理存储的是绝对法线信息,只能作用于创建时对应的模型,应用到其他模型上无法得到正确的效果。而切线空间的法线纹理的坐标系为各自顶点的坐标系,是一个相对法线信息,应用到其他的模型或者一个砖块使用一张法线纹理应用到6个面上也能得到合理的结果。 - 可进行UV动画
切线空间纹理中的法线方向是根据对应纹理的纹理坐标方向得到。因此可以通过移动一个纹理的UV坐标实现凹凸移动的效果。 - 可压缩
切线空间下的法线纹理中,z轴方向总为正方向,因此可以可以仅存储x,y轴方向,通过计算再得到z方向。
切线空间下的法线纹理在使用上更加灵活,因此基本上选用切线空间作为法线纹理的坐标系。
光照模型计算中坐标空间的选择
由于在计算光照过程中,需要在一个统一的空间下进行,而法线纹理所在的空间为切线空间。因此有两种方式选择。
- 将光照方向和视角方向变换到切线空间进行,这个过程在顶点着色器中可以完成。
- 将法线方向变换到世界空间中进行,这时需要先对发现纹理进行采样,所以该过程在片元着色器中进行,并在片元着色器中进行一次矩阵运算。
切线空间下的光照模型计算
由于法线纹理中使用的本身就是切线空间,因此需要将光照方向和观察方向先转换到切线空间,这个过程可以在顶点着色器中完成。模型-->切线空间下的转换矩阵计算,首先切线空间-->模型空间的变换矩阵为模型顶点的切线方向(x轴)、副切线方向(y轴)、法线方向(z轴)的按列排列形式,即模型-->切线空间变换矩阵的逆矩阵,而对于一个方向矢量而言,一个变换矩阵若只存在平移和旋转变换,则该矩阵为一个正交阵,即变换矩阵的逆矩阵与转置矩阵相等,因此模型-->切线空间的变换矩阵为逆矩阵的转置,即将模型顶点的切线方向(x轴)、副切线方向(y轴)、法线方向(z轴)的 按行排列。
实例代码:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter7_NormalMapTangentSpace" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_MainTex("MainTex",2D) ="white" {}
_BumpTex("Noraml Tex",2D) = "bump"{} //bump为Unity自带的法线纹理,当没有提供任何法线时,"bump"就对应模型自身的法线信息
_BumpScale("BumpScal",Float) = 1.0 //BumpScale代表凹凸程度,值为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 _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT; //tangent存储顶点的切线方向,float4类型,通过tangent.w分量决定副切线的方向性
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;//(uv.xy存储主纹理坐标变换后的uv坐标)
o.uv.zw = v.texcoord.xy*_BumpTex_ST.xy + _BumpTex_ST.zw;//(uv.zw存储法线纹理坐标变换后的uv坐标)
//_MainTex和_BumpTex通常会使用同一组纹理坐标(法线纹理贴图由对应纹理贴图生成)
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(_BumpTex,i.uv.zw); //对法线纹理进行采样
fixed3 tangentNormal;
//若法线纹理类型没有被设置为bump类型,则进行手动反映射
//tangentNormal=(packedNormal.xyz*2-1);
//若已经设置为bump类型,可以使用内建函数
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
fixed3 albedo = _Color.rgb*tex2D(_MainTex,i.uv.xy);
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"
}
世界空间下的光照模型计算
世界空间下的光照模型计算需要将切线空间下的法线变换到世界空间中,因此需要先知道切线到世界的变换矩阵,又由于法线是通过在片元着色器中做纹理采样得到,因此需要通过在顶点着色器中得到转换矩阵再传递到片元着色器,在进行纹理采样后再做转换并计算光照效果。
实例代码:
Shader "Custom/Chapter7_WorldNormal" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_MainTex("MainTex",2D) = "white"{}
_BumpTex("BumpTex",2D) = "bump"{}
_BumpScale("BumpScale",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 _BumpTex;
float4 _BumpTex_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;
//定义用于存储变换矩阵的变量,并拆分成行存储在对应行的变量中,
//对于矢量的变换矩阵只需要3X3即可,float4的最后一个值可以用来存储世界空间下顶点的位置
float4 T2W0:TEXCOORD1;
float4 T2W1:TEXCOORD2;
float4 T2W2: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*_BumpTex_ST.xy + _BumpTex_ST.zw;
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent)*v.tangent.w;
o.T2W0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x,worldPos.x);
o.T2W1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.T2W2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) :SV_Target{
float3 worldPos = float3(i.T2W0.w,i.T2W1.w,i.T2W2.w);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//法线纹理采样
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpTex, i.uv.zw));
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
fixed3 worldNormal =normalize( half3(dot(i.T2W0.xyz,tangentNormal),dot(i.T2W1.xyz,tangentNormal),dot(i.T2W2.xyz,tangentNormal)));
fixed3 albedo = _Color.rgb*tex2D(_MainTex,i.uv).rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(worldLightDir,worldNormal));
fixed3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
这里需要注意的是,当定义一个float3,float4类型时,在赋值的右边一定要加上float3,float4关键字,否则可能会得到错误的效果。
float3 worldPos = float3(i.T2W0.w,i.T2W1.w,i.T2W2.w);
o.T2W0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x,worldPos.x);
实例效果:
中间为切线空间下计算的结果,右边为世界空间下的计算的结果,表现效果上并没有区别。
渐变纹理
纹理的最初使用,是为了给一个模型表面上色。实际上,纹理可以用来存储表面属性,如之前的法线纹理将法线信息存储在一张纹理中。通过纹理也可以控制漫反射光照结果。
渐变纹理控制漫反射实例代码:
Shader "Custom/Chapter7_RampTexture" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_RampTex("RampTex",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"
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex:POSITION;
float3 normal:NORMAL;
fixed4 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*_RampTex_ST.xy + _RampTex_ST.zw;
return o;
}
fixed4 frag(v2f i) :SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed halfLambert = 0.5*dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert))*_Color.rgb;
fixed3 diffuse = diffuseColor*_LightColor0.rgb;
fixed3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(halfDir,worldNormal)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
Shader代码中值得注意的地方:
fixed halfLambert = 0.5*dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert))*_Color.rgb;
- 这里进行纹理采样的uv坐标为半兰伯特值,将法线与光照方向的点积映射到[0,1]也就是说原本光照不到的地方会取到渐变纹理中靠左下部分的颜色。
- 由于采样时uv坐标都是相等的,因此取到的颜色应该是对应纹理坐标[0,1]内的对角线上的颜色。
- 还有一点值得注意的是,当采用突变性的渐变纹理时(如第一张渐变纹理),漫反射的结果是阴影之间更加分明,类似于卡通效果。
遮罩纹理
遮罩纹理应用于很多商业游戏中,用来保护某些区域,免于某些修改。两个常见的应用:
- 使模型某些区域的高光强烈,某些区域较弱。而不是将高光反射应用到模型的所有地方,使用遮罩纹理可以更加细腻的控制高光的光照效果。
- 制作地形材质时需要混合多张图片,例如表现草地,石子,裸露土地的纹理。使用遮罩纹理可以控制如何混合这些纹理。
使用者遮罩纹理的流程:通过采样得到遮罩纹理的纹素值,然后使用其中某个通道的值与某种表面属性进行相乘,当该通道值为0时,可以保护表面不受属性影响。
实例代码:
Shader "Custom/Chapter7_MaskTexture" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_MainTex("MainTex",2D) = "white"{}
_BumpTex("BumpTex",2D) = "bump"{}
_BumpScale("BumpScale",Float)=1.0
_SpecularMask("SpecularMask",2D) = "white"{}
_SpecularMaskScale("SpecularMaskScale",Float) = 1.0
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20.0
}
SubShader{
Pass{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST; //主纹理,法线纹理,遮罩纹理的纹理坐标用一个变量来存储
sampler2D _BumpTex;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularMaskScale;
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 = v.texcoord*_MainTex_ST.xy + _MainTex_ST.zw;
float3 binormal = cross(normalize(v.normal),normalize(v.tangent))*v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);
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(_BumpTex,i.uv));
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex,i.uv)*_Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(tangentLightDir,tangentNormal));
fixed3 halfDir = normalize(tangentLightDir+tangentViewDir);
fixed3 specularMask = tex2D(_SpecularMask, i.uv).r*_SpecularMaskScale;//使用其中一个通道r来影响高光的光照效果
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(halfDir,tangentNormal)),_Gloss)*specularMask;
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
这里有两点需要注意的地方:
- 主纹理,法线纹理和遮罩纹理的纹理坐标都来自主纹理的uv坐标,也就是说当修改材质面板的主纹理的缩放和偏移值时,法线纹理和遮罩纹理都会相应变化,而修改法线纹理和遮罩纹理的缩放偏移值是不会对计算结果产生任何影响的,事实上测试的结果也是这样。
- 遮罩纹理的计算过程中只用到了纹理的r通道,其他3个通道其实可以用来存储更多的设置值。
通过遮罩处理后,高光的效果不是全部反映到整个区域,而是由r通道进行选择,遮罩纹理中全黑色的地方,即r=0处是不会受到高光影响的,这也是为什么高光部分的裂缝处的凹槽更加清晰。