1.切线空间
uv空间,u,v定义可以类比顶点坐标系x,y,z.
u表示纹理水平方向,v表示纹理竖直方向
Tangent Space,其实一个坐标系,也就是原点+三个坐标轴决定的一个相对空间,
我们只要搞清楚原点和三个坐标轴是什么就可以了。
在Tangent Space中,
坐标原点就是顶点的位置,
z轴是该顶点本身的法线方向(N)。
(tangent)T 与该点相切的切线,这样的切线本来有无数条,但模型一般会给定该顶点的一个tangent,这个tangent方向一般是使用和纹理坐标方向相同的那条tangent(T)。
(bitangent或者说binormal)B,副切线或者说副法线,是通过normal和tangent的叉乘得到相互垂直的线。
(下面有几张图,来自知乎,侵删)
v2-5f2b99607cf1790cdd955149f0436ae2_hd.jpg v2-cc97e8e2d0eff2084045cf19d61bf58b_hd.jpg v2-af0f3390357b898decbb9e7a50046822_hd.jpg v2-fc03b4d2c6628f502b3a3daf59d290d2_hd.jpg2.切线空间矩阵推导
[图片上传中...(20170228214738798.png-5153d1-1524496131680-0)] 20170228214738798.png该图显示了一个三角形及其所在的切线空间。
注意在局部坐标系里,因为这个局部的切平面式垂直于Z轴即法线的,所以虽然uv坐标式二维的,但是只有加上第三维z等于0即可以当成一个三维坐标使用
接上图我们有如下等式:
p3.jpg part1.jpg
这里要特别注意,需要编辑器的图片材质中把法线贴图类型转换成Normal贴图类型
QQ截图20180425233630.jpg
凹凸为0的效果图
QQ截图20180425233551.jpg
调整凹凸参数得到的效果图
QQ截图20180425233612.jpg
还是很明显的可以看出区别来
shader代码如下
Shader "Custom/NormalMapTangentSpaceMat"
{
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
_MainTex ("Main Texture", 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
{
Tags { "RenderType" = "Opaque"
"LightMode" = "ForwardBase"
}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
#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;
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 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss);
return fixed4(ambient + diffuse+ specular, 1.0);
}
ENDCG
}
}
}
这里用了unity 内建宏定义
TANGENT_SPACE_ROTATION
#define TANGENT_SPACE_ROTATION \
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
1.为啥这里要乘以v.tangent.w,
经过一段时间的搜索找到如下解释,有勘误还请指正
首先我们切换回到纹理的定义
对于纹理UV来说,
OpenGL 从左到右(0,1),从下到上(0,1)
DirectX 从左到右(0,1),从下到上(1,0)
可见对于切线 ,即UV的U,对于OpenGL和DirectX都是左到右(左侧为0.0,右侧为1.0)。
而对于binormal,即UV的V,这在OpenGL和DirectX中是不同的。
OpenGL是自下而上的,DirectX是自上而下的。
这也是许多引擎和3D工具对“+ Y / -Y”法线贴图的偏好来自不同的地方。
Unity是+ Y,这是OpenGL标准,
Unreal是-Y,这是DirectX标准。
显然,Unity在Windows上使用DirectX,所以大部分时间W组件都是负值的。
然而,如果纹理UV被反转,不是因为网格反转,而是因为网格的纹理被镜像。
所以,如果纹理被镜像,则需要w来存储这个值
2.rotation 为什么表达了从object空间到tangent空间的转换
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
首先我们知道float3x3( v.tangent.xyz, binormal, v.normal )是按行优先往矩阵里放东西的,也就是说
这个东西
其实等同于如下矩阵
等同于矩阵
由由于是单位正交矩阵,上述矩阵等同于如下表达式(请查阅相关线性代数只是)
我们知道
中,注意这三个列向量空间是属于Object空间的,所以它表达了从Tangent空间到Object空间的转换关系,关于这一点请参照相关线性代数,尤其要理解列空间基向量的概念。
所以,自然的
表达的是从Object空间到Tangent空间的转换关系。
也即
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
表达了从Object空间到Tangent空间的转换关系。
得证
4.附:世界空间下的法线扰动测试
效果同上,代码如下
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/NormalMapWorldSpaceMat"
{
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
_MainTex ("Main Texture", 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
{
Tags { "RenderType" = "Opaque"
"LightMode" = "ForwardBase"
}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
#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大小的分量,所以这里将矩阵拆分三行向量,
float4 TtoW1:TEXCOORD2;
float4 TtoW2:TEXCOORD3;
float4 TtoW3:TEXCOORD4;
float3 worldPos:TEXCOORD5;
};
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;
o.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;
//注意这里的worldNormal,worldTangent,worldBinormal,均表达了模型空间到世界空间的转换,因此三个列空间基向量排列构成的矩阵表达了从模型空间到世界空间的变换,对此存在疑问的童鞋可以查看相关线性代数课程
//本人也写过线性代数方面的总结性文章,可以查看本人主页查找线性代数。
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, 0);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, 0);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, 0);
o.TtoW3 = float4(0, 0, 0, 1);//存粹为了好看,其实这些完全可以省略
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldPos = i.worldPos;
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//将切线空间的法线转化到世界空间。
fixed3 worldNormal = normalize(fixed3(dot(i.TtoW0.xyz,tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));
///same
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, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient + diffuse+ specular, 1.0);
}
ENDCG
}
}
}
网友评论