纹理的另一种常见的应用就是凹凸映射(bump mapping)。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。
(1)有两种主要的方法可以用来进行凹凸映射:
(2) 法线纹理是如何存储表面的法线方向的?
(3)法线纹理中存储的法线方向的坐标空间:
【注意】
:这两个函数的功能都是获取从顶点出发的世界空间下的光源方向。但一个参数是模型空间的顶点坐标,另一个参数是世界空间的顶点坐标。
//【模型空间顶点】
WorldSpaceLightDir(v.vertex)
//【世界空间顶点】
UnityWorldSpaceLightDir(worldPos)
重点是vert函数和frag函数,注释中包含具体步骤。
Shader "ShaderBook/Chapter7/NormalMap" {
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"
//物体颜色属性
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 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
v2f vert(a2v v) {
//【1】定义返回数据类型
v2f o;
//【2】转换顶点坐标到裁剪空间,存到返回结构体中
o.pos = UnityObjectToClipPos(v.vertex);
//【3】把纹理坐标存到返回的结构体中
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//【4】得到世界空间下的顶点位置、法线、切线、副切线
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//【5】构建切线空间到世界空间的矩阵,即切线、副切线、法线垂直堆叠。
// 最后一列存储世界空间顶点位置。
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 {
//【1】物体颜色:主纹理 x 颜色
//获取纹理颜色:tex2D(name,uv).rgb
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
//【2】环境光:主灯光颜色 x 物体颜色
//主灯光颜色:UNITY_LIGHTMODEL_AMBIENT.xyz
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//【3】漫反射:主灯光颜色 x 物体颜色 x 漫反射系数(世界空间法线和灯光方向的点积)
// A. 世界空间法线:
//UnpackNormal()函数是对法线纹理的采样结果的一个反映射操作,其对应的法线纹理需要设置为Normal map的格式,才能使用该函数。
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));//从法线纹理中采样,并反映射到(-1,1)范围内
bump.xy *= _BumpScale;//法线的xy分量是必要的,进行scale
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));//法线的z分量根据xy分量计算得到
//把法线从切线空间转换到世界空间: 3x3矩阵 乘以 3x1矩阵 得到 3x1矩阵。
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
// B. 世界空间顶点位置:TtoW矩阵的最后一列
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// C. 灯光方向:根据世界空间的顶点位置得到:UnityWorldSpaceLightDir(worldPos)
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
//【4】高光:灯光颜色 x 高光颜色 x 高光系数
//高光系数:法线和视角方向的点积的幂
//视角方向:根据世界空间的顶点位置得到:UnityWorldSpaceViewDir(worldPos)
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
//【5】最后的颜色:环境光+漫反射+高光
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
当我们把纹理类型设置成Normal map时到底发生了什么呢?简单来说,这么做可以让Unity根据不同平台对纹理进行压缩,再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样
。
在某些平台上由于使用了DXT5nm的压缩格式,因此需要针对这种格式对法线进行解码。在DXT5nm格式的法线纹理中,纹素的a(rgba)通道(即w分量)对应了法线的x分量,g通道对应了法线的y分量,而纹理的r和b通道则会被舍弃,法线的z分量可以由xy分量推导而得。
重点是vert函数和frag函数,注释中包含具体步骤。
Shader "ShaderBook/Chapter7/NormalMapTagent" {
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"
//物体颜色属性
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) {
//【1】定义返回数据类型
v2f o;
//【2】转换顶点坐标到裁剪空间,存到返回结构体中
o.pos = UnityObjectToClipPos(v.vertex);
//【3】把纹理坐标存到返回的结构体中
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//【4】构建世界空间到切线空间的矩阵
// A. 先得到世界空间下的切线、副切线、法线。
// 可以直接构建出切线空间到世界空间的矩阵。切线、副切线、法线垂直堆叠。
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// B. 如果一个变换中仅存在平移和旋转变换,那么这个变换的逆矩阵就等于它的转置矩阵.
//而从切线空间到模型空间的变换正是符合这样要求的变换。
//所以将切线、副切线、法线横向堆叠就好。
float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
//【5】把灯光方向、视角方向从世界空间转换到切线空间,存到返回结构体中
//时间空间的灯光、视角方向可以从模型空间的顶点位置得到。
o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
//【6】另一种更简单的把灯光和视角向量变换到切线空间的方法
//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 {
//【1】计算物体颜色
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
//【2】计算环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//【3】计算漫反射
// A. 切线空间灯光方向
fixed3 tangentLightDir = normalize(i.lightDir);
// B.切线空间法线法向
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)));
// C.漫反射公式
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
//【4】计算高光
// A. 切线空间视角方向
fixed3 tangentViewDir = normalize(i.viewDir);
// B. 高光公式
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
//【5】 最终颜色
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}