目录
- 矩阵与矩阵变换
- 坐标系统
- OpenGL的矩阵与矩阵变换
- 实践:平移、旋转、缩放、3D
- 资料
- 收获
一、矩阵与矩阵变换
1.1 矩阵的基本知识回顾
OpenGL大量使用向量和矩阵,矩阵的最重要的用途之一就是建立向量投影(比如:正交和透视投影)、使物体旋转(rotation)、平移(translation)以及缩放(scaling)。下面我们来介绍下几个常用的矩阵类型。
单位矩阵
缩放矩阵
旋转矩阵
平移矩阵
矩阵乘法
矩阵变换时用到的乘法是左乘,不符合交换律,但遵循结合定律
AB != BA
C(BA)=(CB)A=CBA
C(B(Av))=(CBA)v
正交投影矩阵
投影矩阵
齐次坐标
什么是齐次坐标?
3维数据可以通过3维向量与3x3矩阵的乘法操作,来完成缩放和旋转的线性变换。但是对于笛卡尔坐标(欧氏坐标)的平移操作是时加法操作,这样无办法做到变换的线性连续性。这就引入了齐次坐标,齐次坐标用N+1维来代表N维坐标。在3D笛卡尔坐标尾部加上一个额外的变量就变成了3D齐次坐标
一个点(X,Y,Z)在齐次坐标中变成了(x,y,z,w)
并且 X= x/w; Y= y/w; Z=z/w
引入齐次坐标后平移也可以通过矩阵相乘的来表示了,保证了形式上的线性一致性,从而做到不管怎么样变换,变换多少次,都可以表示成一连串的矩阵相乘,真是太方便了。
在OpenGL中对于3维数据引入的第四分量事实就是用来实现透视投影变换的。
齐次坐标的作用,合并矩阵运算中的乘法和加法,把各种变换都统一了起来,即 把缩放,旋转,平移等变换都统一起来,都表示成一连串的矩阵相乘的形式。保证了形式上的线性一致性
二、坐标系统
2.1 左手坐标和右手坐标系统
大拇指指向x轴正方向、其余是个手指指向y轴正方向,分别用左手和右手来向内握拳就表示上述的不同的坐标系。归一化设备坐标使用的左手坐标系统,而OpenGL早期的版本使用的却是右手系统,这也是Android的Matrix会默认生成反转z的矩阵的原因。
2.2 物体坐标系(建模坐标系)
是一个局部坐标系,为了模型与变换的方便。比如,在创建圆形时,一般将圆心作为参考点来创建圆周上的各个点,实际上就是构建了一个以圆心为原点的参考坐标系。
2.3 世界坐标系
对物体建模之后,下一步是将各个图形组合放到绘制平面中,为了确定每个图形的位置,必须放弃各自的物体坐标系(建标坐标系),将其纳入一个统一的坐标系,这个坐标系就称为世界坐标系
2.4 眼睛坐标系(观察坐标系)
用户可根据图形显示的要求定义观察区域和观察方向,即要定义视点(或者成为照相机)的位置和方向,从观察者的角度对整个世界坐标系内的图形进行重新定位和描述。
2.5 归一化设备坐标系(规范化设备坐标系)
为了使观察处理独立于输出设备,将观察坐标系的对象描述转换到一个中间坐标系,这个坐标系即独立于设备,又可以容易地转变为设备坐标系。其坐标范围为【0,1】提高应用的可移植性。
2.6 设备坐标系(屏幕坐标系)
主要用于某一特殊的计算机图形显示设备表面的像素定义,对于每个具体的显示设备,都有一个单独的坐标系。
三、OpenGL的矩阵变换
回顾、学习了上述基础知识后,我们来看下OpenGL的矩阵变换
顶点着色器的位置输入保存为物体坐标,而输出位置保存为裁剪坐标。
模型—视图—投影(MVP)矩阵
模型矩阵——将物体坐标变换为世界坐标
视图矩阵——将世界坐标变换为眼睛坐标
投影矩阵——将眼睛坐标变换为裁剪坐标(齐次坐标)
在传统的OpenGL功能中,模型和试图合并为一个矩阵,成为模型-视图矩阵,将物体坐标变换为眼睛坐标。空间中任意位置和任何想要的方向都可以由一个4X4矩阵唯一确定,并且如果用一个对象的所有向量乘以这个矩阵,那么我们就将整个对象变换到了空间中的给定位置和方向。可以用glRotatef、glTranslatef、glScalef等函数创建,需要应用程序自己处理
视图变换
视图变换允许我们把观察点放在所希望的任何位置,并允许在任何方向上观察场景。确定视图变换就像在场景中放置照相机并让它指向某个方向。模型变换
模型变换用于操纵模型和其中的特定对象。这些变换将对象移动到需要的位置,然后再对它们进行旋转和缩放。
投影变换
投影变换将在模型视图变换之后应用到顶点上,它将指定一个完成的场景(所有模型变换都已完成)是如何投影到屏幕上的最终图像。
正投影:所有多边形都是精确地按照指定的相对大小来在屏幕上绘制的。
透视投影:透视投影的特点是透视缩短,这种特性使得远处的物体看起来比进出同样大小的物体更小一些。视口变换
当所有变换完成后,就得到了一个场景的二维投影,它将被映射到屏幕上某处的窗口上。这种到物理窗口坐标的映射是我们最后要做的变换,称为视口变换。
图片来自《OpenGL编程指南》(红宝书)
四、实践
4.1 平移、旋转、缩放
//定义顶点着色器 //定义一个不经常更改的mat4矩阵uniform mat4 u_Matrix; attribute vec4 a_Position;attribute vec4 a_Color; varying vec4 v_Color; void main() { v_Color = a_Color; //gl_Position的赋值采用矩阵和a_Position相乘 gl_Position = u_Matrix * a_Position;}
Render关键代码
@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { Log.i(TAG, "onSurfaceCreated: curThread= " + Thread.currentThread()); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); String vertexCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "vertex_shader.glsl"); String fragmentCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "fragment_shader.glsl"); //创建着色器程序 programId = ShaderHelper.loadProgram(vertexCode, fragmentCode); uMatrixLocation = GLES20.glGetUniformLocation(programId, U_MATRIX); int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION); Log.i(TAG, "drawFrame: aposition="+aPosition); mVertexData.position(0); GLES20.glVertexAttribPointer(aPosition, COORDS_PER_VERTEX,//用几个偏移描述一个顶点 GLES20.GL_FLOAT,//顶点数据类型 false, STRIDE,//一个顶点需要多少个字节偏移 mVertexData//分配的buffer ); //开启顶点着色器的attribute GLES20.glEnableVertexAttribArray(aPosition); int aColor = GLES20.glGetAttribLocation(programId, A_COLOR); mVertexData.position(COORDS_PER_VERTEX); GLES20.glVertexAttribPointer(aColor,COLOR_PER_VERTEX,GL_FLOAT,false,STRIDE,mVertexData); GLES20.glEnableVertexAttribArray(aColor); } @Override public void onDrawFrame(GL10 gl) { Log.i(TAG, "onDrawFrame: " + "curThread= " + Thread.currentThread()); GLES20.glClear(GL_COLOR_BUFFER_BIT); GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, modelMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,6); }
下面的平移、旋转、缩放等都是再onSurfaceChanged进行模型矩阵的赋值。
原图
乘以单位矩阵效果是和原来一模一样的
Matrix.setIdentityM(modelMatrix,0);
进行缩放
Matrix.setIdentityM(modelMatrix,0); Matrix.scaleM(modelMatrix,0,0.5f,0.5f,0);
进行旋转, 没有设置投影和z方向的偏移,所以还是一个平面图
Matrix.setIdentityM(modelMatrix,0); Matrix.rotateM(modelMatrix,0,-60,1,0,0);
进行平移
Matrix.setIdentityM(modelMatrix,0);Matrix.translateM(modelMatrix,0,0.3f,0.3f,0);
4.2 通过添加w实现3D效果
private final static int COORDS_PER_VERTEX = 4; float[] TRIANGLE_COORDS = { // x,y,z,w. r,g,b 0.5f, 0.5f,0,5f, 1f, 0.5f,0.5f, -0.5f, -0.5f,0, 1f, 0.5f, 1f,0.5f, 0.5f, -0.5f,0,1f, 0.5f, 0.5f,1f, 0.5f, 0.5f,0,5f, 1f, 0.5f,0.5f, -0.5f, 0.5f,0,5f, 0.5f, 0.5f,1f, -0.5f, -0.5f,0,1f, 0.5f, 1f,0.5f,}
效果如下:
Render的完整代码如下:
public class MyRender implements GLSurfaceView.Renderer { private static final String TAG = "MyRender"; private int programId; //每个顶点坐标的个数 private final static int COORDS_PER_VERTEX = 4; //每个顶点颜色的个数 private final static int COLOR_PER_VERTEX = 3; private final static int BYTES_PER_FLOAT = 4; private static final String A_POSITION = "a_Position"; private static final String A_COLOR = "a_Color"; //-个float占用4个字节,STRIDE是一个点的字节偏移 private final int STRIDE = (COORDS_PER_VERTEX+ COLOR_PER_VERTEX )* BYTES_PER_FLOAT; private FloatBuffer mVertexData; public MyRender2() { //顶点坐标数组 float[] TRIANGLE_COORDS = { 0.5f, 0.5f,0,5f, 1f, 0.5f,0.5f, -0.5f, -0.5f,0, 1f, 0.5f, 1f,0.5f, 0.5f, -0.5f,0,1f, 0.5f, 0.5f,1f, 0.5f, 0.5f,0,5f, 1f, 0.5f,0.5f, -0.5f, 0.5f,0,5f, 0.5f, 0.5f,1f, -0.5f, -0.5f,0,1f, 0.5f, 1f,0.5f, }; mVertexData = ByteBuffer .allocateDirect(TRIANGLE_COORDS.length * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(TRIANGLE_COORDS); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { Log.i(TAG, "onSurfaceCreated: curThread= " + Thread.currentThread()); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); String vertexCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "vertex_shader.glsl"); String fragmentCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "fragment_shader.glsl"); //创建着色器程序 programId = ShaderHelper.loadProgram(vertexCode, fragmentCode); int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION); Log.i(TAG, "drawFrame: aposition="+aPosition); mVertexData.position(0); GLES20.glVertexAttribPointer(aPosition, COORDS_PER_VERTEX,//用几个偏移描述一个顶点 GLES20.GL_FLOAT,//顶点数据类型 false, STRIDE,//一个顶点需要多少个字节偏移 mVertexData//分配的buffer ); //开启顶点着色器的attribute GLES20.glEnableVertexAttribArray(aPosition); int aColor = GLES20.glGetAttribLocation(programId, A_COLOR); mVertexData.position(COORDS_PER_VERTEX); GLES20.glVertexAttribPointer(aColor,COLOR_PER_VERTEX,GL_FLOAT,false,STRIDE,mVertexData); GLES20.glEnableVertexAttribArray(aColor); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { Log.i(TAG, "onSurfaceChanged: width=" + width + " h=" + height + "curThread= " + Thread.currentThread()); GLES20.glViewport(0, 0, width, height); } @Override public void onDrawFrame(GL10 gl) { Log.i(TAG, "onDrawFrame: " + "curThread= " + Thread.currentThread()); GLES20.glClear(GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,6); }}
但是w的值是在代码中写死的,下面我们透过投影矩阵的方式自动生成w值,为动态的改变做好铺垫。
4.3 投影矩阵实现3D
首选需要修改顶点着色器,gl_position的赋值不是完全通过glVertexAttribPointer,而是借用矩阵相乘的方式实现。具体顶点着色器代码如下:_
//定义一个不经常更改的mat4矩阵uniform mat4 u_Matrix; attribute vec4 a_Position;attribute vec4 a_Color; varying vec4 v_Color; void main() { v_Color = a_Color; //gl_Position的赋值采用矩阵和a_Position相乘 gl_Position = u_Matrix * a_Position;}
下面我们在Render中给u_Matrix矩阵赋值_
首先定义四个矩阵变量和matlocatin
private final float[] projectionMatrix = new float[16]; //4*4的投影矩阵 private final float[] modelMatrix = new float[16]; //4*4的模型矩阵 private final float[] viewMatrix = new float[16];//4*4d的视图矩阵 private final float[] mvpMatrix = new float[16];//4*4d的MVP矩阵 private int uMatrixLocation;//glsl中的uniform mat4变量的location
然后在onSurfaceChanged 给上述矩阵赋值
@Override public void onSurfaceChanged(GL10 gl, int width, int height) { Log.i(TAG, "onSurfaceChanged: width=" + width + " h=" + height + "curThread= " + Thread.currentThread()); GLES20.glViewport(0, 0, width, height); //设置模型矩阵 即M Matrix.setIdentityM(modelMatrix,0); Matrix.translateM(modelMatrix,0,0,0,-2.5f); Matrix.rotateM(modelMatrix,0,-60,1,0,0); //设置视图矩阵 即V Matrix.setLookAtM(viewMatrix,0,0,0,1,0,0,0,1,0,0); //设置投影矩阵 即P Matrix.perspectiveM(projectionMatrix,0,60,(float) width/ height,1f,200f); Matrix.multiplyMM(projectionMatrix,0,projectionMatrix,0,modelMatrix,0); Matrix.multiplyMM(mvpMatrix,0,projectionMatrix,0,viewMatrix,0); }
最后再onDrawFrame中调用
GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mvpMatrix, 0);
效果如下:
五、资料
《计算机图形学基础(OpenGL版)》
《OpenGL ES 3.0 编程指南》
《OpenGL编程指南》(红宝书)
《OpenGL ES应用开发实践指南》
[OpenGL 矩阵变换(讲的太好了~!)]
[ songho ]
[为什么要引入齐次坐标,齐次坐标的意义]
[OpenGL入门第三课--矩阵变换与坐标系统]
[Android OpenGL ES(二)-正交投影]
[OpenGL坐标变换]
[OpenGL坐标系解析(顶点从对象坐标系到屏幕坐标系的计算流程)]
[OpenGL中各种坐标系的理解]
[OpenGL透视投影下的模型视图矩阵/投影矩阵/观察者矩阵]
六、收获
这篇文章的信息量很大,最重要的是对概念的理解清楚,而代码实现上各个平台都有提供封装比较好的方法比如Matrix。
- 回顾了矩阵的基本知识(单位矩阵、正交矩阵、投影矩阵、矩阵相乘规则等)和左右手坐标,
- 学习了很多新的概念,比如世界坐标、齐次坐标、视口坐标、归一化坐标、设备坐标、视图变换、模型变换、投影变换、视口变换等。为了搞懂这些基本的概念查看了多本书籍,才把这些基本概念搞明白。
- 介绍android中Matrix类的API
- 实践了3D效果、实现平移、缩放、旋转。
感谢你的阅读
下一篇我们一起来学习实践OpenGL的纹理贴图,欢迎关注公众号“音视频开发之旅”,一起学习成长。
原文地址 :https://mp.weixin.qq.com/s/WSHSByhBd7uxX2E4HSD9lg
本文暂时没有评论,来添加一个吧(●'◡'●)