1,将向量由世界空间变换至切线空间
2,将向量由切线空间变换至世界空间
1,将向量由世界空间变换至切线空间
假设在世界空间中有一个球体,还有一个光源,光源的方向接近于正对球体点p的法线。
图1
在点p处存在着一个切线空间,在此空间中,p的法线作为z轴,p的tangent作为x轴,p的binormal由normal和tangent叉乘而得,作为y轴。tangent由点所在的三角形中的uv方向确定(三角形中的uv值)将light direction移到此切线空间:
图2
接下来要寻找Light Direction在切线空间中的值,首先将light direction起始点移到点P:
图3
接线来light direction变为一个3*1的矩阵,并乘以一个由Tangent,Binormal,Normal组成的3*3矩阵,所得结果既是light direction在切线空间中的值。
那么为什么乘以此矩阵既能做到坐标系变换呢?先将此矩阵乘法中的三次行列相乘变为三个点乘:
点乘的一个重要性质是投影,projection。上面的三个点乘可在3D空间中具象化:
dot(Normal,LightDirection)
图4
dot(Tangent,LightDirection)
图5
dot(Binormal,LightDirection)
图6
Pn,Pt,Pb为三次点乘后的结果。以Binormal为例,解释一下上图中的计算过程。在Unity中,网格顶点的Normal,Tangent的长度均为1,因此叉乘的Binormal长度也为1。然后将点乘进行展开,dot(Binormal,LightDirection)=|Binormal|*|LightDirection|*cos
图7
所以
图8
图5中的角
切线空间的一个重要应用是凹凸贴图,正确的凹凸贴图需要将法向量贴图中的法线从切线空间转移到世界空间,或将光线从世界空间转移到切线空间。
图8(左边为未进行切线空间转换的贴图效果,右边为进行了切线空间转换的贴图效果)
示例Shader代码:
Shader "Unlit/C8E5v_TangentSpaceBump"
{
Properties
{
_BumpMap ("BumpMap", 2D) = "white" {}
_LightPos("Light Position",vector)=(5,5,5)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
float3 lightDir:float;
};
sampler2D _BumpMap;
float4 _BumpMap_ST;
float4 _LightPos;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _BumpMap);
UNITY_TRANSFER_FOG(o,o.vertex);
o.lightDir=_LightPos-v.vertex.xyz;
float3 binormal=cross(v.normal,v.tangent);
//用顶点的Tangent,Binormal,Normal组合成选择矩阵
float3x3 rotation=float3x3(v.tangent.xyz,binormal,v.normal);
//将光向量转移至切线空间,使用mul方法,括号中右边向量会变为1*3的矩阵,返回值会再变回向量
o.lightDir=mul(rotation,o.lightDir);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 normalTex = UnpackNormal(tex2D(_BumpMap, i.uv)).xyz;
float3 normal=normalize(normalTex);
i.lightDir=normalize(i.lightDir);
fixed4 col=dot(normal,i.lightDir);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
2,将向量由切线空间变换至世界空间
矩阵乘法的性质:
已知向量v,可逆矩阵M
再观察矩阵M和它的转置矩阵:
M:
此矩阵有一个特点,
证明方法则是将Tangent,Binormal,Normal整体移到世界坐标原点并旋转至与世界坐标xyz轴重叠,那么:
当
带入
也就是说向量v在被正交矩阵M变换后,可用M的转置矩阵变换回原来的向量。
那么将切线空间中的向量变换至世界空间既是:
示例代码:
Unity官网示例Shader,注意tspace的使用:
Shader "Unlit/SkyReflection Per Pixel"
{
Properties {
// normal map texture on the material,
// default to dummy "flat surface" normalmap
_BumpMap("Normal Map", 2D) = "bump" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float3 worldPos : TEXCOORD0;
// these three vectors will hold a 3x3 rotation matrix
// that transforms from tangent to world space
half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
// texture coordinate for the normal map
float2 uv : TEXCOORD4;
float4 pos : SV_POSITION;
};
// vertex shader now also needs a per-vertex tangent vector.
// in Unity tangents are 4D vectors, with the .w component used to
// indicate direction of the bitangent vector.
// we also need the texture coordinate.
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(_Object2World, vertex).xyz;
half3 wNormal = UnityObjectToWorldNormal(normal);
half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
// compute bitangent from cross product of normal and tangent
half tangentSign = tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
// output the tangent space matrix
//转置矩阵!
o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
o.uv = uv;
return o;
}
// normal map texture from shader properties
sampler2D _BumpMap;
fixed4 frag (v2f i) : SV_Target
{
// sample the normal map, and decode from the Unity encoding
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
// transform normal from tangent to world space
half3 worldNormal;
//使用点乘代替乘以矩阵
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);
// rest the same as in previous shader
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
————————————————————————————————————————————————
Reference:
1,Cg Tutorial-Nvidia
2,http://www.jmargolin.com/uvmath/uvmath.htm –Jed Margolin
3,https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html –Unity
4,https://betterexplained.com/articles/vector-calculus-understanding-the-dot-product/ –BetterExplained
更新日志:
2017-6-23:更改了标题和一些错误。
2017-6-24:增加了reference。
2017-8-22:更改了标题。