一.先看看实现效果图
(左边的2d图片如何运动出右边3d的效果)
引言:
对于这个题目,真的很尴尬,不知道取啥,就想了这个题目,涵盖范围很广,很抽象,算是通用知识点吧。想要了解下面几个问题的,可以看看。
①2D图形如何运动出3D空间的效果。
②3D物体如何渲染成2D图形到屏幕上。
③Unity中模型到世界,世界到相机,相机到屏幕的关系。
④如何通过矩阵进行各种风骚(旋转,缩放,平移,投影等)的变换操作。
二.应用知识
①向量
②矩阵,矩阵变换规则
③透视投影
三.实现
问题:图形不断变换,通过简化,图形的本质是由顶点组合而成,因此,可以简化为顶点不断变换。不断意思大概就是每隔一段时间变换,我们这里
再简化,可以简化为变换一次。即问题可以不断简化如图
对于“顶点变换“,顶点,即是向量,根据矩阵的相关知识,我们可以了解到,变换矩阵可以使向量得到指定的变换。因此就是“顶点通过矩阵变换”。
最后,问题的本质即为顶点和矩阵的之间的交互即可。
1)顶点
顶点,当然是拥有x,y,z三个分量,根据矩阵变换规则,我们想要使用矩阵对向量进行变换,需要多一个维度,且第四个分量为1。至于具体原因这里不加详述,
详情可见:https://blog.csdn.net/zl_gsyy/article/details/73278742。
即,需要设定一个拥有四维向量Vector4.cs
class Vector4 { public double a, b, c, d; public Vector4() { } public Vector4(double a,double b,double c, double d) { this.a = a; this.b = b; this.c = c; this.d = d; } public Vector4(Vector4 v) { this.a = v.a; this.b = v.b; this.c = v.c; this.d = v.d; } }
2)矩阵
因为我们需要变换的是4维向量(顶点,可以看做1X4的矩阵),所以再根据矩阵变换规则,我们需要设定矩阵维4x4的。
定义 Matrix4x4.cs
class Matrix4x4 { double[,] ts; public Matrix4x4() { ts = new double[4, 4]; } public Matrix4x4(Matrix4x4 m) { for(int i=0;i<4;i++) { for(int j=0;j<4;j++) { ts[i, j] = m[i, j]; } } } public double this[int x,int y] { get{ return ts[x, y]; } set { ts[x, y] = value; } } /// <summary> /// 用这个矩阵变换一个四维向量,返回另外一个向量 /// </summary> /// <param name="v"></param> /// <returns></returns> public Vector4 Mul(Vector4 v) { Vector4 vNew = new Vector4(); vNew.a = ts[0, 0] * v.a + ts[1, 0] * v.b + ts[2, 0] * v.c + ts[3, 0] * v.d; vNew.b = ts[0, 1] * v.a + ts[1, 1] * v.b + ts[2, 1] * v.c + ts[3, 1] * v.d; vNew.c = ts[0, 2] * v.a + ts[1, 2] * v.b + ts[2, 2] * v.c + ts[3, 2] * v.d; vNew.d = ts[0, 3] * v.a + ts[1, 3] * v.b + ts[2, 3] * v.c + ts[3, 3] * v.d; return vNew; } }
根据矩阵变换规则,如果需要变换多次,多次变换可以通过相乘合并为一个矩阵,比如某顶点需要先平移,再缩放,可以平移*缩放=合并矩阵。
所以需要在Matrix4x4.cs 中 增加一个矩阵*矩阵得到另外一个矩阵 方法。
public Matrix4x4 Mul(Matrix4x4 m) { Matrix4x4 newM = new Matrix4x4(); for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { for (int k = 0; k < 4; k++) { newM[i, j] += this[i, k] * m[k, j]; } } } return newM; }
3)模拟变换过程
因为我们这边直接使用点来变换显示,不够直观,可以使用由3个顶点组成的三角形,来模拟测试效果。所以,这里再定义一个三角形类Triangles.cs.
直接使用矩阵对该Triangles进行变换,即是对三个顶点进行变换。
class Triangles { public Vector4 A, B, C; private Vector4 a, b, c; //临时变量使用 public Triangles(Vector4 a,Vector4 b,Vector4 c) { A =this.a= new Vector4(a); B =this.b= new Vector4(b); C =this.c= new Vector4(c); } /// <summary> /// 变换过后原始顶点保留(变换针对原始数据) /// </summary> /// <param name="m"></param> public void Transform(Matrix4x4 m) { this.a = m.Mul(this.A); this.b= m.Mul(this.B); this.c = m.Mul(this.C); } #region 系统内置方法(这边了解一下就行) /// <summary> /// 应用windows窗体应用程序内部方法,根据三个顶点画出三角形 /// </summary> /// <param name="e"></param> public void Draw(System.Drawing.Graphics e) { e.TranslateTransform(300, 300);//不然会太靠左上角。Windowform的内部功能 e.DrawLines(new Pen(Color.Red, 2f), Get2DPointFArr()); } PointF[] Get2DPointFArr() { PointF[] points = new PointF[4]; points[0] = Get2DPointF(this.a); points[1] = Get2DPointF(this.b); points[2] = Get2DPointF(this.c); points[3] = Get2DPointF(this.a); //我们这边需要画第四个与第一个顶点一样,系统需求这样做 return points; } PointF Get2DPointF(Vector4 v) { PointF point = new PointF(); point.X = (float)(v.a / v.d); point.Y = (float)(v.b / v.d); return point; } #endregion }
通过以上,我们可以实现通过变换矩阵(平移,缩放,旋转)来实现简单变换了。当然通过某种变换的时候首先需要明白对应的哪个矩阵。参考https://www.cnblogs.com/u3ddjw/p/10282186.html
======================================================正式进入正题=====================================================================
4)如何实现3D空间变换效果
①首先,我们先声明一个三角形,并且给出其三个顶点的坐标。
Triangles triangles; //注意这边屏幕左上角为(0,0)坐标原点 Vector4 v1 = new Vector4(0, -0.5, 0, 1); Vector4 v2 = new Vector4(0.5f, 0.5, 0, 1); Vector4 v3 = new Vector4(-0.5f, 0.5f, 0, 1); triangles = new Triangles(v1, v2, v3);
因为我们这里给出坐标位置是比例坐标(模型坐标),这种大小当然很小,与屏幕(1320,1080)差距很大,所以我们需要把它放大道合适的大小。假设就放大250倍刚好是我们需要的大小。就需要缩放矩阵了,通过https://www.cnblogs.com/u3ddjw/p/10282186.html 查找到缩放矩阵 。
故,我声明scale =250,
//模型到世界 m_scale = new Matrix4x4(); m_scale[0, 0] = scale; //scale =250,这个数值可以任意调节,你可以试试改变这个大小,加强得到缩放的效果的记忆。 m_scale[1, 1] = scale; m_scale[2, 2] = scale; m_scale[3, 3] = 1; triangles.Transform(m_scale);
最终一张正常的图出现:
②为了模拟出直观的3D效果,再来实现把这个三角形进行旋转
参考上面方法找到旋转矩阵表达式,为了观察直观,我们每隔一小段时间增加2个角度并显示出来。我们增加个Timer(就是可以实现
间隔时间调用方法的工具类,系统一般都会自带,比如unity中update)
float a = 2; /// <summary> /// 每隔0.02s调用一次 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void timer1_Tick(object sender, EventArgs e) { a++; double angle = a / 360 * Math.PI; m_rotation[0, 0] = Math.Cos(angle); m_rotation[0, 2] = Math.Sin(angle); m_rotation[1, 1] = 1; m_rotation[2, 0] = -1 * Math.Sin(angle); m_rotation[2, 2] = Math.Cos(angle); m_rotation[3, 3] = 1; //因为矩阵变换规则得知多次变换可以先将变换矩阵相乘 缩放矩阵*旋转矩阵 var newTriangle = m_scale.Mul(m_rotation); triangles.Transform(newTriangle); this.Invalidate(); //重绘,必须写上 }
得到结果:
上图总是变宽变宰,下面底部线条没有感觉在3D空间中变换。这不是真正意义3D变换, 仅仅是模型到世界。
③透视投影变换
假设右边的圈就是我们需要拍摄的摄像机空间的点,还需要进行透视投影变换到屏幕。故,我们还需要世界到摄像机,
要让他从世界到摄像机, 所以我们还需要架设一个摄像机去拍摄他,假设这个模型在z轴为0的位置,摄像机假设到z为负的位置,对于模型而言
在摄像机空间上实际上做了位置平移。所以我们需要一个可以转换为摄像机空间的矩阵(平移矩阵)。
同上找到平移矩阵,并初始化一个平移矩阵,平移多少位置?还是先假设250(这边可以自行修改数据调节看看效果)。
故在初始化函数中,
//View 世界到相机 m_view = new Matrix4x4(); m_view[0, 0] = 1; m_view[1, 1] = 1; m_view[2, 2] = 1; m_view[3, 2] = 1 * scale; m_view[3, 3] = 1;
目前效果上还是没有变换的,虽然平移了摄像机,但是我们没有真正的做投影变换。关于投影矩阵,还是参考https://www.cnblogs.com/u3ddjw/p/10282186.html,
找到投影矩阵。继续在初始化函数中并实现
//Projection 相机到屏幕 m_projection = new Matrix4x4(); m_projection[0, 0] = 1; m_projection[1, 1] = 1; m_projection[2, 2] = 1; m_projection[2, 3] = 1.0f / scale;
在timer.cs 中的 timer1_Tick 每隔一段时间函数中补充为
/// <summary> /// 每隔0.02s调用一次 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void timer1_Tick(object sender, EventArgs e) { a+=3; double angle = a / 360 * Math.PI; m_rotation[0, 0] = Math.Cos(angle); m_rotation[0, 2] = Math.Sin(angle); m_rotation[1, 1] = 1; m_rotation[2, 0] = -1 * Math.Sin(angle); m_rotation[2, 2] = Math.Cos(angle); m_rotation[3, 3] = 1; //因为矩阵变换规则得知多次变换可以先将变换矩阵相乘 缩放矩阵*旋转矩阵 var newTriangle = m_scale.Mul(m_rotation); newTriangle = newTriangle.Mul(m_view); newTriangle = newTriangle.Mul(m_projection); triangles.Transform(newTriangle); this.Invalidate(); //重绘,必须写上 }
这样,最终我们看到了第一张图的效果图,完全的3D空间的效果。现在是具有透视变换的三角形在屏幕上不停的旋转,绕着y轴。
四.拓展
①可以尝试改变各个变换矩阵中的值,比如平移矩阵(世界到相机)的值,来看看效果。
private void trackBar1_Scroll(object sender, EventArgs e) { m_view[3, 2] = (sender as TrackBar).Value; } private void trackBar2_Scroll(object sender, EventArgs e) { double value = (sender as TrackBar).Value; m_projection[2, 3] = 1.0f / value; }
②不用三角形,自己尝试五角星,四边形之类的变换,效果各种拉风,但是本质还是都是顶点通过矩阵的变换。
③实际应用,由上述可见Unity中最常见的MVP变换:模型到世界,世界到相机,相机到屏幕 的变换。本质上也分别是缩放+旋转+平移,缩放+旋转+平移,投影 矩阵的变换。