線性代數:仿射變換圖形矯正

        之前學習完矩陣的理解和作用,又經歷過一輪基本仿射變換推導,我想大家對矩陣在實際程序中的應用應該基本瞭解了,這裏我們就實際應用一下。

        之前學習的變換過程基本都是變換一個“規範”的圖形,這次我們就反過來,把一個“不規範”的圖形變換“規範”。

        首先作爲碼農們,我們應該都會閱讀大量書籍的,但是爲了方便我自己下載過大量pdf文檔,因爲那樣我不需要隨身帶一本厚重厚重的書,只用帶個ipad就ok了,不過我下載的很多pdf的掃描頁面歪七歪八的,比如像下面這樣:

        

        看完這種掃描頁面的pdf簡直就是治療了我的頸椎病(ps:本圖並不是針對矩陣論這本書,只是打個誇張的比方而已)。其實我們現實中書本都是四四方方的,我們當然也希望下載的pdf頁面都規整了,下圖是實體書籍:

        

        如果要我們把上面扭曲的頁面變成下面的四四方方的頁面要怎麼辦?其實仔細一想,這就是一個仿射變換的過程,前面我們談過仿射變換的特點,那就是“平直性”,也就是說圖形變換後,圖形邊是直線還是直線,是平行線的還是平行線。我們的處理方法可以思考一下,無非就是讓仿射矩陣T將扭曲的頁面的每個網格頂點和四四方方的頁面的網格頂點相對應。

        既然瞭解到這是個仿射變換的過程,那麼我們就建立仿射變換矩陣T變換一下就ok了。這樣我們就來考慮怎麼計算出這個變換的矩陣T呢?前面我們推導矩陣一般都是使用已知的“映射座標點”帶入矩陣*向量構建的線性方程組中,既然如此就先建立仿射變換矩陣T,然後建立好若干“映射座標點”。

        我們可以把頁面的四個角的座標點當作“映射座標點”,然後標識出座標信息用來計算,如下圖:

        

        

        上面兩幅圖我創建了對應正正方方圖形比例的rectangle拓撲網格承載兩幅圖片(不清楚怎麼創建拓撲mesh的小夥伴可以看前面的博客,這裏我就不貼重複mesh代碼),形象的展示了下兩張紋理圖的“映射座標點”,因爲我們的目的是爲了將扭曲圖頂點變換爲正方圖頂點,所以我們可以參考下幅圖中兩圖的仿射座標系原點重合的情況,不考慮矩陣平移維度。這樣問題就變成了推導3x3的無“平移維度”的仿射變換矩陣T,所以只需要三個“映射座標點”就可以了,so我們開始矩陣的推導,如下圖:

        

        我來解釋下解法,首先我們建立矩陣T,然後建立三個映射座標點,於是我們建立好了矩陣*向量的線性方程組。接下來我們組合出a1b1c1的三元一次方程組(同樣也能建立a2b2c2和a3b3c3的三元一次方程組),通過消元法,先消去a1,得到b1c1的二元一次方程組,接着分別消元得到b1c1的值就行了。因爲是三元一次方程組,所以算出來的代數式還是很複雜的。

        接下來我們就消元b1和c1來求出a1,如下圖:    

        

        這樣的話我們就把矩陣的第一行[a1  b1  c1]求出來了,因爲我們全部使用的代數式,所以只需要替換m n u v x y的代數就能得到[a2 b2 c2]和[a3 b3 c3]了,如下圖:

        

        其實最開始組建的一個三元一次方程組和上圖的三元一次方程組區別就在於n v y分量不同而已,我們只需要替換掉n v y分量就行了,下圖的[a3  b3  c3]一樣的,如下圖:

        

        這時候我們的矩陣T就建立完畢了,當然實際使用中我們要擴充“平移維度{0 0 0 1}”得出一個無平移的4x4矩陣T'用來進行齊次座標的變換,那麼我們接下來繼續寫程序。

        

Shader "Unlit/CorrectUnlitShader"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_PNGCut("PNGCut",Range(0,1)) = 0.5
		_M("M",vector) = (0,0,0,1)
		_U("U",vector) = (0,0,0,1)
		_X("X",vector) = (0,0,0,1)
		_N("N",vector) = (0,0,0,1)
		_V("V",vector) = (0,0,0,1)
		_Y("Y",vector) = (0,0,0,1)
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
		Cull Off

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float _PNGCut;  //png圖片alpha剔除閥值

			vector _M;  //映射起點1
			vector _U;  //映射起點2
			vector _X;  //映射起點3

			vector _N;  //映射終點1
			vector _V;  //映射終點2
			vector _Y;  //映射終點3;

			vector _Scale;  //不規範網格縮放矯正參數

			//獲取矩陣的一行,因爲代數式n v y分量的替換性
			float4 getMatrixRow(float n, float v, float y, vector m, vector u, vector x)
			{
				float4 row = float4(0, 0, 0, 1);
				row.x = ((n*u.y - v*m.y)*(u.z*x.y - u.y*x.z) - (v*x.y - y*u.y)*(m.z*u.y - m.y*u.z)) / ((m.x*u.y - m.y*u.x)*(u.z*x.y - u.y*x.z) - (u.x*x.y - u.y*x.x)*(m.z*u.y - m.y*u.z));
				row.y = ((n*u.x - v*m.x)*(x.x*u.z - x.z*u.x) - (v*x.x - y*u.x)*(m.z*u.x - m.x*u.z)) / ((m.y*u.x - m.x*u.y)*(x.x*u.z - x.z*u.x) - (x.x*u.y - x.y*u.x)*(m.z*u.x - m.x*u.z));
				row.z = ((n*u.x - v*m.x)*(x.x*u.y - x.y*u.x) - (v*x.x - y*u.x)*(m.y*u.x - m.x*u.y)) / ((m.z*u.x - m.x*u.z)*(x.x*u.y - x.y*u.x) - (x.x*u.z - x.z*u.x)*(m.y*u.x - m.x*u.y));
				row.w = 0;
				return row;
			}

			v2f vert (appdata v)
			{
				v2f o;
				//構建矯正的變換矩陣,擴充的平移維度
				float4x4 _Mat_correct = float4x4(getMatrixRow(_N.x, _V.x, _Y.x, _M, _U, _X),
												getMatrixRow(_N.y, _V.y, _Y.y, _M, _U, _X),
												getMatrixRow(_N.z, _V.z, _Y.z, _M, _U, _X),
												0, 0, 0, 1);
				float4 vx = mul(_Mat_correct, v.vertex);
				o.vertex = UnityObjectToClipPos(vx);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				clip(col.a - _PNGCut);
				return col;
			}
			ENDCG
		}
	}
}

        

        上面的cgshader代碼,我們把映射點當作參數傳入,然後再vert頂點函數中建立對應的頂點映射變換矩陣,然後使用c#代碼進行控制參數。

    

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CorrectCtrl : MonoBehaviour {

    public Renderer mRender;

    public Transform MPoint;
    public Transform UPoint;
    public Transform XPoint;

    public Transform NPoint;
    public Transform VPoint;
    public Transform YPoint;

    private Vector4 mM;
    private Vector4 mU;
    private Vector4 mX;

    private Vector4 mN;
    private Vector4 mV;
    private Vector4 mY;

    private Material mMat;

    void Start()
    {
        mMat = mRender.sharedMaterial;

        mM = getTransformVector(MPoint);
        mU = getTransformVector(UPoint);
        mX = getTransformVector(XPoint);

        mN = getTransformVector(NPoint);
        mV = getTransformVector(VPoint);
        mY = getTransformVector(YPoint);

        mMat.SetVector("_M", mM);
        mMat.SetVector("_U", mU);
        mMat.SetVector("_X", mX);

        mMat.SetVector("_N", mN);
        mMat.SetVector("_V", mV);
        mMat.SetVector("_Y", mY);

#if UNITY_EDITOR  //驗證方程組解法是否正確
        Matrix4x4 mat = getMatrixCorrect();
        Debug.LogFormat("mat = {0}", mat);
        Vector4 r1 = mat * mM;
        Vector4 r2 = mat * mU;
        Vector4 r3 = mat * mX;
        if (r1 == mN && r2 == mV && r3 == mY)
        {
            Debug.Log("equation right");
        }
#endif
    }

    /// <summary>
    /// 轉換成齊次座標表示
    /// </summary>
    /// <param name="p"></param>
    /// <returns></returns>
    private Vector4 getTransformVector(Transform p)
    {
        return new Vector4(p.position.x, p.position.y, p.position.z, 1);
    }

    /// <summary>
    /// 使用c#語言測試獲取矩陣
    /// </summary>
    /// <returns></returns>
    private Matrix4x4 getMatrixCorrect()
    {
        Vector4 row0 = getMatrixRow(mN.x, mV.x, mY.x, mM, mU, mX);
        Vector4 row1 = getMatrixRow(mN.y, mV.y, mY.y, mM, mU, mX);
        Vector4 row2 = getMatrixRow(mN.z, mV.z, mY.z, mM, mU, mX);
        Matrix4x4 mat = new Matrix4x4();
        mat.m00 = row0.x;
        mat.m01 = row0.y;
        mat.m02 = row0.z;
        mat.m03 = row0.w;
        mat.m10 = row1.x;
        mat.m11 = row1.y;
        mat.m12 = row1.z;
        mat.m13 = row1.w;
        mat.m20 = row2.x;
        mat.m21 = row2.y;
        mat.m22 = row2.z;
        mat.m23 = row2.w;
        mat.m30 = 0;
        mat.m31 = 0;
        mat.m32 = 0;
        mat.m33 = 1;
        return mat;
    }

    private Vector4 getMatrixRow(float n, float v, float y, Vector4 m, Vector4 u, Vector4 x)
    {
        Vector4 row = new Vector4(0, 0, 0, 1);
        row.x = ((n * u.y - v * m.y) * (u.z * x.y - u.y * x.z) - (v * x.y - y * u.y) * (m.z * u.y - m.y * u.z)) / ((m.x * u.y - m.y * u.x) * (u.z * x.y - u.y * x.z) - (u.x * x.y - u.y * x.x) * (m.z * u.y - m.y * u.z));
        row.y = ((n * u.x - v * m.x) * (x.x * u.z - x.z * u.x) - (v * x.x - y * u.x) * (m.z * u.x - m.x * u.z)) / ((m.y * u.x - m.x * u.y) * (x.x * u.z - x.z * u.x) - (x.x * u.y - x.y * u.x) * (m.z * u.x - m.x * u.z));
        row.z = ((n * u.x - v * m.x) * (x.x * u.y - x.y * u.x) - (v * x.x - y * u.x) * (m.y * u.x - m.x * u.y)) / ((m.z * u.x - m.x * u.z) * (x.x * u.y - x.y * u.x) - (x.x * u.z - x.z * u.x) * (m.y * u.x - m.x * u.y));
        row.w = 0;
        return row;
    }
}    

        上面的c#代碼是控制參數傳入,同時矩陣創建我也用c#代碼實現,爲了log一下驗證推導的正確性,這時候啓動程序,會看到扭曲的頁面的網格頂點被矩陣變換成四方頁面的網格頂點,這樣就做到了圖形矯正的作用,如下圖:

         

        上面展示了傳入參數然後cg中計算矩陣最後變換達成效果。

        demo下載地址:https://download.csdn.net/download/yinhun2012/10321497

        

        

        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章