之前学习完矩阵的理解和作用,又经历过一轮基本仿射变换推导,我想大家对矩阵在实际程序中的应用应该基本了解了,这里我们就实际应用一下。
之前学习的变换过程基本都是变换一个“规范”的图形,这次我们就反过来,把一个“不规范”的图形变换“规范”。
首先作为码农们,我们应该都会阅读大量书籍的,但是为了方便我自己下载过大量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
————————————————
版权声明:本文为CSDN博主「羊羊2035」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yinhun2012/article/details/79707714

