Unity Shader:向量在世界空间与切线空间中变换的相关数学原理详解

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在切线空间中的值。

TangentxBinormalyNormalzTangentxBinormalyNormalzTangentxBinormalyNormalzLightDirection.xLightDirection.yLightDirection.z=LightDirection.xTLightDirection.yTLightDirection.zT

那么为什么乘以此矩阵既能做到坐标系变换呢?先将此矩阵乘法中的三次行列相乘变为三个点乘:

TangentxLightDirection.x+TangentyLightDirection.y+TangentzLightDirection.z
=dot(Tangent,LightDirection)
BinormalxLightDirection.x+BinormalyLightDirection.y+BinormalzLightDirection.z
=dot(Binormal,LightDirection)
NormalxLightDirection.x+NormalyLightDirection.y+NormalzLightDirection.z
=dot(Normal,LightDirection)

点乘的一个重要性质是投影,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 θ =|LightDirection|*cos θ 。然后将Pb与LightDirection组成的直角三角形转移到平面几何中分析。直角三角形中的cos θ 等于两邻边之比:

这里写图片描述
图7

cosθ=|Pb||LightDirection|
所以 cosθ|LightDirection|=|Pb| ,也既是点乘dot(Binormal,LightDirection)的结果。那么Pb的意义是什么呢?假如将图6中的Binormal看成是一个3D空间坐标系中的y轴,那么Pb既是LightDirection.y在此坐标系中值,依次类推,将三个投影值组合起来,(Pt,Pb,Pn)既是向量LightDirection在此切线空间中新的值。
这里写图片描述
图8
图5中的角 θ 大于了90度,由于cosine的性质,它得出的将是一个负数。而图4中的 θ 接近于180度,它接近于负1,得出的值接近于-|LightDirection|。

切线空间的一个重要应用是凹凸贴图,正确的凹凸贴图需要将法向量贴图中的法线从切线空间转移到世界空间,或将光线从世界空间转移到切线空间。

这里写图片描述
图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
Mv=v
v=M1v

再观察矩阵M和它的转置矩阵:
M:

TangentxBinormalyNormalzTangentxBinormalyNormalzTangentxBinormalyNormalz

MT:
TangentxTangentyTangentzBinormalxBinormalyBinormalzNormalxNormalyNormalz

此矩阵有一个特点, MMT 一定会等于一个单位矩阵:
I=100010001
证明方法则是将Tangent,Binormal,Normal整体移到世界坐标原点并旋转至与世界坐标xyz轴重叠,那么:
M=100010001MT=100010001MMT=I

MMT=I ,M叫做正交矩阵,正交矩阵的一个性质既是它的转置矩阵等于它的逆矩阵:

MT=M1

带入 v=M1v
v=MTv

也就是说向量v在被正交矩阵M变换后,可用M的转置矩阵变换回原来的向量。

那么将切线空间中的向量变换至世界空间既是:
TangentxTangentyTangentzBinormalxBinormalyBinormalzNormalxNormalyNormalzXTYTZT=XWYWZW

示例代码:

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:更改了标题。

猜你喜欢

转载自blog.csdn.net/liu_if_else/article/details/73604356