8.GLES类及Matrix类.md
June 16, 2025 · View on GitHub
8.GLES类及Matrix类
GLES作为我们与着色器连接的工具类提供了丰富的api。了解一些常用API的意思能更方便后面的学习,这里就简单整理列了一些常用的部分:
-
glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha)
为颜色缓冲区指定清除值
-
glClear(GLbitfileld mask)
清除预设值的缓冲区,参数分别为GL_COLOR_BUFFER_BIT(当前启用了颜色写入的缓冲区) GL_DEPTH_BUFFER_BIT(深度缓冲区) GL_STENCIL_BUFFER_BIT(模板缓冲区)
-
glActiveTexture(GLenum texture)
激活纹理单元。参数指定要激活的纹理单元,参数texture必须是GL_TEXTUREi之一,初始值为GL_TEXTURE0。
-
glBindTexture(GLenum target, GLunit texture)
将一个特定的纹理ID绑定到一个纹理目标上
-
glGenTextures(GLsizei n, GLunit * textures)
生成纹理ID,参数n是指定要生成的纹理ID的数量,textures是指定存储生成的纹理ID的数组
-
glCreateShader(GLenum shaderType)
创建一个空的着色器对象,类型分别为GL_VERTEX_SHADER和GL_FRAGMENT_SHADER
-
glCompileShader(GLunit shader)
编译一个着色器对象。创建后要先编译才能运行。
-
glAttachShader(GLunit program, GLunit shader)
将着色器对象附加到program对象
-
glCreateProgram()
创建一个空的program对象并返回一个可以被引用的program id。
-
glEnableVertexAttribArray(GLunit index)/glEnableVertexAttribArray(GLunit index)
启用或禁用通用定点属性数组。参数为指定要启用或禁用的通用顶点属性的索引。
-
glDrawArrays(GLenum mode, GLinit first, GLsizei count)
参数mode为指定要渲染的图元类型,如GL_POINTS,GL_LINE_STRIP,GL_LINE_LOOP。first为指定已启用阵列中的起始索引。count为指定要渲染的索引数。在调用该方法前,可以使用glVertexAttribPointer预先指定单独的顶点、位置和颜色数组等信息。
-
glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices)
mode是指定要渲染的图元类型,可以为GL_POINTS,GL_LINE_STRIP,GL_LINES等,count指定要渲染的元素数,type指定indices中的类型,必须是GL_UNSIGNED_BYTE或GL_UNSIGNED_SHORT。indices指定指向存储索引的位置的指针。使用该方法前也是需要使用glVertexAttribPointer来预先指定单独的顶点位置和颜色数据等信息。
-
void glGetUniformfv(GLunit program, GLinit location, GLfloat *params)
通过params的形式返回指定统一变量的值,参数program指定要查询的program对象,location指定要查询的统一变量的位置,params返回指定的统一变量的值。
-
void glGetVertexAttribfv(GLunit index, GLenum pname, GLFloat *params)
以params形式返回通用顶点属性参数的值。参数index是指定要查询的通用顶点的属性参数,pname是指定要查询的顶点属性参数的符号名称。可接受的值为GL_VERTEX_ATTRIB_ARRAY_BFFER_BINGDING等。
-
void glGetVertexAttribPointerv(GLunit index, GLenum pname, GLvoid **pointer)
以**pointer返回指定的通用顶点属性的指针信息。参数index为要返回单额通用顶点属性采参数,pname是指定要返回的通用顶点属性参数的符号名称,必须是GL_VERTEX_ATTRIB_ARRAY_POINTER。
-
glLinkProgram(GLunit program)
链接program指定的program对象,指定要链接的program对象的句柄。
-
glTexImage2D(GLenum target, Glint level, GLinit internalformat, GLsizei width, GLsizei height, GLinit border, GLenum format, GLenum type, const GLvoid *data)
指定一个二维的纹理图片。纹理将指定纹理图像的一部分映射到纹理化为活动的每个图形基元。
-
void glUniform1f(GLinit location, GLfloat v0)
-
void glUniformxx(GLinit l n,b njiu[uyi77u777uocation, GLfloat v0)
-
void glUniformMatrix1fx(GLinit location, GLsizei countM, GLboolean transpose, const GLfloat *valueM)
修改统一变量或统一变量数组的值。参数location为指定要修改的统一变量的位置,可以由glGetUniformLocation返回,v就是用于指定统一变量的新值。
count是指定要修改的元素数,value是指定指向将用于更新指定统一变量的count值数组的指针。countM指定要修改的矩阵数。
-
glUseProgram(GLunit program)
使用程序对象program作为当前渲染状态的一部分。
-
glVertexAttrib(GLunit index, GLFloat vo ...)
指定通用顶点属性的值
-
glVertexAttribPointer(GLunit index, GLinit size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer)
指定索引index处的通用顶点属性数组的位置和数据格式,以便在渲染时使用。
获取着色器程序内成员变量的id(句柄、指针)
mPostionHandler = GLES30.glGetAttribLocation(mProgram, "aPosition"):获取着色器程序中,指定为attribute类型的变量id。
mMatrixHnadler = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"):获取着色器程序中,指定为uniform类型的变量id。
向着色器传递数据
上面获取到指向着色器中相应数据成员的各个id后,就能将我们要设置的顶点数据、颜色数据等传递到着色器中了。
// 将最终变换矩阵传入shader程序
GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
// 顶点位置数据传入着色器
GLES30.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer);
// 顶点颜色数据传入着色器中
GLES30.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer);
// 顶点坐标传递到顶点着色器
GLES30.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer);
定义顶点属性数组
/**
* glVertexAttribPointer()方法的参数分别为:
* index:顶点属性的索引.(这里我们的顶点位置和颜色向量在着色器中分别为0和1)layout (location = 0) in vec4 vPosition; layout (location = 1) in vec4 aColor;
* size: 指定每个通用顶点属性的元素个数。必须是1、2、3、4。此外,glvertexattribpointer接受符号常量gl_bgra。初始值为4(也就是涉及颜色的时候必为4)。
* type:属性的元素类型。(上面都是Float所以使用GLES30.GL_FLOAT);
* normalized:转换的时候是否要经过规范化,true:是;false:直接转化;
* stride:跨距,默认是0。(由于我们将顶点位置和颜色数据分别存放没写在一个数组中,所以使用默认值0)
* ptr: 本地数据缓存(这里我们的是顶点的位置和颜色数据)。
*/
GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer);
启用或禁用顶点属性数组
调用GLES30.glEnableVertexAttribArray和GLES30.glDisableVertexAttribArray传入参数index。
如果启用,那么当GLES30.glDrawArrays或者GLES30.glDrawElements被调用时,顶点属性数组会被使用。
尽管现在已经知道了如何创建一个物体、着色、加入纹理,给它们一些细节的表现,但因为它们都还是静态的物体,仍不够有趣。
我们可以尝试着在每一帧改变物体的顶点并且重配置缓冲区从而使它移动,但这太繁琐了,而且会消耗很多的处理时间。
我们现在有一个更好的解决方案,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。
为了更好的了解矩阵,需要先了解向量.
向量
向量有一个方向(Direction)和大小(Magnitude,也叫强度或长度)。
你可以把向量想象成一个藏宝图上的指示: 向左走10步,向北走3步,然后向右走5步。
这里面左就是方向,10步就是向量的长度。
向量可以在任何维度(Dimension)上,但是我们通常只使用2至4维。
如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像)。
当它有3个维度的时候它可以表达一个3D世界的方向。

上图中有3个向量,每个向量在2D图像中都用一个箭头(x, y)表示。
由于向量表示的是方向,起始于何处并不会改变它的值。
例如上面的向量v和w是相等的,尽管他们的起始点不同。
由于向量是一个方向,所以有些时候会很难形象的将它们用位置(Position)表示出来。
为了让其更为直观,我们通常设定这个方向的原点为(0, 0, 0),然后指定一个方向,对应一个点,使其变为位置向量(Position Vector)。
你也可以把起点设置为其他的点,然后说这个向量从这个点起始指向另一个点。
比如说位置向量(3, 5)在图像中的起点会是(0, 0),并会指向(3, 5)。我们可以使用向量在2D或3D空间中表示方向与位置。
向量相乘
两个向量相乘是一种很奇怪的情况。
普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。
但是在相乘的时候我们有两种特定情况可以选择:
- 一个是点乘(Dot Product)
- 一个是叉乘(Cross Product)
点乘
两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。
叉乘
叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。
如果输入的两个向量也是正交的,那么叉乘之后将会产生3个相互正交的向量。
下面的图就是3D控件中叉乘的样子:

Matrix
Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的,里面所有的实现原理都是线性代数中的运算。
我们知道OpenGl中实现图形的操作大量使用了矩阵,在OpenGL中使用的向量为列向量,我们通过利用矩阵与列向量(颜色、坐标都可看做列向量)相乘,得到一个新的列向量。
利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。
Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例。
OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时,只能在列向量前面乘以矩阵。
而行向量则反之,否则乘法没有意义。
-
multiplyMM
两个4x4矩阵相乘,并将结果存储到第三个4x4矩阵中。
public static native void multiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset); -
multiplyMV
将一个4x4矩阵和一个四元素向量相乘,得到一个新的四元素向量
public static native void multiplyMV(float[] resultVec,int resultVecOffset, float[] lhsMat, int lhsMatOffset, float[] rhsVec, int rhsVecOffset); -
transposeM
获取逆矩阵
平移、旋转、缩放等基本变换都是通过将表示点坐标的向量与特定的变换矩阵相乘完成的,进行基于矩阵的变换时,三维空间中点的位置需要表示成齐次坐标形式。
所谓齐次坐标形式也就是在x、y、z 3个坐标值后面增加第四个量w,未变换时w值一般为1。
所谓齐次坐标表示就是用N+1维坐标表示N维坐标。
齐次坐标是为了兼容点的平移操作,使得我们可以用同一个公式对点和方向做运算。 y 采用齐次坐标是由于很多在N维空间中难以解决的问题在N+1维空间中会变得比较简单。
旋转变换
OpenGL ES中,旋转角度的正负可以用右手螺旋定则来确定。

所谓右手螺旋定则是指:
右手握住旋转轴,使大拇指指向旋转轴的正方向,4指环绕的方向即为旋转的正方向,也就是旋转角度为正值。
基本变换的实质
上面说到的3中基本变换的实现方法,给人的感觉是通过矩阵直接实现了对物体的变换。
但在OpenGL ES中这并不完全准确,若仅仅这样来理解在非常简单的情况下可能没有问题,但在多个物体的组合变换中可能就会产生问题了。
这是因为变换实际上并不是直接针对物体的,而是针对坐标系进行的。
OpenGL ES中变换的实现机制可以理解为首先通过矩阵对坐标系进行变换,然后根据传入渲染管线的原始顶点坐标在最终变换结果坐标系中的位置来绘制。
相机
顶点着色器可赋予程序员一次操作一个顶点(“按顶点”处理)的能力,片段着色器(稍后会看到)可赋予程序员一次操作一个像素(“按片段”处理)的能力,几何着色器可赋予程序员一次操作一个图元(“按图元”处理)的能力。
到目前为止,我们所接触的变换矩阵全都可以在3D空间中操作。但是,我们最终需要将3D空间或它的一部分展示在2D显示器上。
为了达成这个目标,我们需要找到一个有利点。
正如我们在现实世界通过眼睛从一点观察一样,我们也必须找到一点并确立观察方向作为我们观察虚拟世界的窗口。这个点叫作视图或视觉空间,或“合成相机”(简称相机)。
观察3D世界需要:
- 将相机放入世界的某个位置;
- 调整相机的角度,通常需要一套它自己的直角坐标轴u、v、n(由向量U,V,N构成);
- 定义一个视体(view volume);将视体内的对象投影到投影平面(projection plane)上。

OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图所示:

为了应用OpenGL相机,我们需要将它移动到适合的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置。
给定世界空间中的点PW,我们需要通过变换将它转换成相应相机空间中的点,从而让它看起来好像是从我们期望的相机位置CW看到的样子。
从日常生活的经验中可以很容易的了解到,随着摄像机位置、姿态的不同,就算是对同一个场景进行拍摄,得到的画面也是迥然不同的。
因此摄像机的位置、姿态在OpenGL应用程序的开发中就显得非常重要。
摄像机的设置需要给出3个方面的信息,包括摄像机的位置、观察的方向以及up方向,

- 摄像机的位置很容易理解,用其在3D空间中的坐标来表示。
- 摄像机观察的方向可以理解为摄像机镜头的指向,用一个观察目标点来表示(通过摄像机位置与观察目标点可以确定一个向量,此向量即代表了摄像机观察的方向)。
- 摄像机的up方向可以理解为摄像机顶端的指向,用一个向量来表示。
通过摄像机拍摄场景与人眼观察现实世界很类似,因此,通过人眼对现实世界观察的切身感受可以理解摄像机的哥哥参数:

从上图可以看出,摄像机的位置、朝向、up方向可以有很多不同的组合。
例如: 同样的位置可以有不同的朝向,不同的up方向。
当我们设置好相机之后,就可以学习投影矩阵了。我们需要学习的两个重要的投影矩阵:透视投影矩阵和正射投影矩阵。
透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。
在正射投影中,平行线仍然是平行的,即不使用透视,如下图所示。正射与透视相反,在视体中的物体不因其与相机的距离而改变,可以直接投影。正射投影是一种平行投影,其中所有的投影过程都沿与投影平面垂直的方向进行。

我们最后要学习的变换矩阵是LookAt矩阵。当你想要把相机放在某处并看向一个特定的位置时,就需要用到它了,LookAt矩阵的元素如下图所示。
当然,用我们已经学到的方法也可以实现LookAt变换,但是这个操作非常频繁,因此为它专门构建一个矩阵通常比较有用。

-
invertM
计算正交投影和透视投影
正交投影
-
orthoM
计算正交投影矩阵
OpenGL ES 3.0中,根据应用程序中提供的投影矩阵,管线会确定一个可视空间区域,称为视景体。视景体是由6个平面确定的,这6个平面分别为:上平面(up)、下平面(down)、左平面(left)、右平面(right)、远平面(far)、近平面(near)。场景中处于视景体内的物体会被投影到近平面上(视景体外面的物体将被裁剪掉),然后再将近平面上投影出的内容映射到屏幕上的视口中。
OpenGL ES 3.0中,根据应用程序中提供的投影矩阵,管线会确定一个可视空间区域,称为视景体。视景体是由6个平面确定的,这6个平面分别为:上平面(up)、下平面(down)、左平面(left)、右平面(right)、远平面(far)、近平面(near)。场景中处于视景体内的物体会被投影到近平面上(视景体外面的物体将被裁剪掉),然后再将近平面上投影出的内容映射到屏幕上的视口中。
对于正交投影而言,视景体及近平面的情况如下图所示:

视点为摄像机的位置;离视点较近(距离为near),垂直于观察方向向量的平面为近平面,离视点较远(距离为far),垂直于观察方向向量的平面为远平面。与观察向量平行,从上下左右4个方向约束视景体范围的4个平面分别为上平面、下平面、左平面、右平面,这4个平面与视景体中心轴线的距离分别为top、bottom、left、right。
由于正交投影是平行投影的一种,其投影线(物体的顶点与近平面上投影点的连线)是平行的。故其视景体为长方体,投影到近平面上的图形不会产生真实世界中“近大远小”的效果:

通过Matrix类的orthoM方法完成对正交投影的设置,其基本代码如下:
Matrix.orthoM (
mProjMatrix, // 生成矩阵元素的float[]类型数据
0, // 起始偏移量
left, right, // near面的left right
bottom, top, // near面的bottom, top
near, far, // near面、far面与视点的距离
)
orthoM方法的功能为根据接收的6个正交投影相关参数产生正交投影矩阵,并将矩阵的元素填充到指定的数组中。
参数left, right为近平面左右侧边对应的x坐标,top、bottom为近平面上下侧边对应的y坐标,分别用来确定左平面、右平面、上平面、下平面的位置。
参数near、far粉笔嗯为视景体近平面与远平面距视点的距离。
场景中的物体投影到近平面后,最终会映射到显示屏上的视口中。
视口也就是显示屏上指定的矩形区域,通过如下代码进行设置:
GLES30.glViewport(x, y, width, height);
从近平面到视口的映射是由渲染管线自动完成的。

上图是由一组距离观察点原来越远的相同尺寸的六角星构成。由于采用的投影方式为正交投影,因此,最终显示在屏幕上的每个六角星尺寸都相同。
透视投影
现实世界中人眼观察物体时会有“近大远小”的效果。
因此,想要开发出更加真实的场景,仅使用正交投影是远远不够的,这时可以采用透视投影。
透视投影的投影线是不平行的,他们相交于视点。
通过透视投影,可以产生现实世界中“近大远小”的效果,大部分3D游戏采用的都是透视投影。
透视投影中,视景体位锥台形区域,如下图:

视点为摄像机的位置;离视点较近(距离为near),垂直于观察方向向量的平面为近平面;离视点较远(距离为far),垂直于观察方向向量的平面为远平面。近平面左侧距中心的距离为left,右侧为right,上侧为top,下侧为bottom。由观察点与近平面左上、左下、右上、右下4个的点连线与远平面的交点可以确定上、下、左、右4个斜面,这4个斜面及远近平面确定了视景体的范围。
透视投影的投影线互不平行,都相交于视点。因此,同样尺寸的物体,近处的投影出来大,远处的投影出来小,从而产生了现实世界中“近大远小”的效果

通过Matrix类的frustumM方法完成对透视投影的设置:
Matrix.frustumM (
mProjMatrix, // 要填充矩阵元素的float[]类型数组
0, // 填充起始偏移量
left, right, // near面的left、right
bottom, top, // near面的bottom、top
near, far // near面、far面与视点的距离
)
从上述代码中可以看出,frustumM方法的功能是,根据接收的6个透视投影相关参数产生透视投影矩阵,并将矩阵的元素依次填充到指定的数组中。
透视投影:

上图由一组距离观察点越来越远的相同尺寸的六角星构成。由于采用的投影方式为透视投影,因此,最终显示在屏幕上的多个六角星近大远小。
-
frustumM
计算透视投影矩阵
-
perspectiveM
根据视场角度、纵横比和Z裁剪平面定义投影矩阵
-
length
计算向量长度
-
setIdentityM(float[] sm, int smOffset)
用来创建一个单位矩阵。其中第一个参数是创建出来的单位矩阵存储的地方,是一个float类型的一维数组。第二个参数是存储的数据位置的偏移量,也就是说从哪里开始存储。生成的结果先按照列优先存储的,也就是说先存放第一列的数据,再存放第二列的数据,以此类推。前面理论部分已经提到,所有变换都是基于单位矩阵的基础上进行的,所以第一步创建单位矩阵是必须的。
-
scaleM(float[] m, int mOffset, float x, float y, float z)
用来进行图像的缩放,第一个参数是需要变换的矩阵;第三、四、五个参数分别对应x,y,z 方向的缩放比例,当x方向缩放为0.5时,相当于向x方向缩放为原来的0.5倍,其他类似。
-
translateM( float[] m, int mOffset, float x, float y, float z)
用来进行图像的位移,第一个参数是需要变换的矩阵;第二个参数是偏移量;第三、四、五个参数分别对应x,y,z 方向的位移量。其以图像自身x,y,z方向为单位,也就是说当x方向位移量为0.5时,相当于向右移动0.5个身位,其他类似。
-
rotateM(float[] m, int mOffset, float a, float x, float y, float z)
用来进行旋转变换的。第一个参数是需要变换的矩阵;第二参数是偏移量;第三个参数是旋转角度,这边是以角度制,也就是说是0-360这个范围;第四、五、六个参数分别代表旋转轴向量的x,y,z值。如果x=0,y=0,z = 1 就相当于以z轴为旋转轴进行旋转,其他类似。
-
setLookAtm
Matrix.setLookAtm(
mVMatrix, // 存储生成矩阵元素的float[]类型数组
0, // 填充起始偏移量
cx, cy, cz, // 摄像机位置的x、y、z坐标
tx, ty, tz, // 观察目标点的x、y、z坐标
upx, upy, upz, // up向量在x、y、z轴上的分量
)
定义相机视图: 从上面代码中可以看出,setLookAtM方法的功能为根据接收的9个摄像机相关参数产生摄像机的观察矩阵,并将矩阵的元素填充到指定的数组中。
之所以产生矩阵是因为OpenGL ES渲染管线的需要。
旋转
OpenGL ES中,旋转角度的正负可以用右手螺旋定则来确定。 所谓右手螺旋定则是指:右手握住旋转轴,使大姆指指向旋转轴的正方向,4指环绕的方向即为旋转的正方向,也就是旋转角度为正值。

- 邮箱 :charon.chui@gmail.com
- Good Luck!