目前我们一直是在竖屏情况下进行学习调试的,那如果我们在横屏下进行调试,会发生什么,很明显宽高比发生了变化,导致我们的矩形,从竖形变成了横形,看起来好像被压扁了,效果如下图:
之所以会这样,是因为我们之前直接把坐标传递给OpenGL时,没有考虑屏幕的宽高比。每个二维和三维应用程序都有一个共同的问题。
在开发图形应用程序时,我们经常面临一个挑战,如何在不同尺寸和方向的屏幕上正确地显示内容。这个问题在二维和三维图形编程中尤为常见。OpenGL作为一个强大的图形库,提供了一种解决方案,那就是使用投影技术来将三维世界映射到二维屏幕上。
投影技术的核心是使用矩阵变换来适应不同的屏幕尺寸和方向。这样,无论用户使用的是手机、平板电脑还是桌面显示器,图形都能以正确的比例和形状显示出来。例如,如果我们没有正确地应用投影,一个矩形可能在某些设备上看起来会被压扁或拉伸。
为了解决这个问题,我们需要首先回顾一些基本的线性代数知识,了解矩阵和向量是如何进行乘法运算的。这将帮助我们理解如何通过矩阵来定义和实现投影变换。
接下来,我们将学习如何利用投影矩阵来调整图形的显示,以适应屏幕的方向变化。通过这种方式,我们可以确保无论屏幕如何旋转或改变大小,图形都能保持其正确的形状和比例,例如,一个桌子在任何情况下看起来都应该是矩形的,而不是被压扁的。
通过学习和应用投影技术,我们可以确保我们的图形应用程序能够更好地适应各种不同的显示设备,提供更加一致和准确的视觉体验。
我们将继续在上一篇的基础上进行本篇内容的开发。
宽高比问题
在OpenGL中,我们通常将所有要渲染的物体映射到一个标准化的坐标空间内,这个空间的x轴和y轴范围是(−1,1),z轴也是如此。这种坐标系统称为归一化设备坐标,它与屏幕的实际尺寸和形状无关。
然而,这种坐标系统虽然方便,但在实际应用中可能会遇到问题。比如,如果我们直接使用归一化设备坐标而不进行任何调整,那么在不同的屏幕方向下,物体的形状可能会被扭曲。举例来说,如果一个设备的实际分辨率是1920像素宽和1080像素高,那么在竖屏模式下,
(−1,1)的坐标范围会对应到1920像素的高度,但只有1080像素的宽度。这会导致图像在水平方向上显得有些扁。同样,在横屏模式下,问题会以垂直方向上的扁形式出现。
归一化设备坐标假设坐标空间是一个正方形,但实际的屏幕视口可能不是正方形。这就可能导致图像在屏幕的一个方向上被拉伸,在另一个方向上被压缩。例如,在一个竖屏设备上,原本应该呈现为矩形的物体,可能会在水平方向上被压扁,而在横屏模式下,物体可能会在垂直方向上出现类似的变形。
要解决这个问题,我们需要考虑屏幕的实际尺寸和形状,通过适当的投影变换来调整物体的显示,确保它们在不同方向的屏幕上都能正确地显示,保持其应有的形状和比例。
适应宽高比
在处理屏幕显示内容时,我们经常需要考虑屏幕的宽高比。为了适应不同的屏幕尺寸和方向,我们可以采用一种简单的方法来调整坐标空间。这种方法的核心思想是保持一个维度的坐标范围不变,而根据屏幕的宽高比来调整另一个维度的坐标范围。
例如,如果我们有一个竖屏设备,其宽度为720像素,高度为1280像素,我们可以选择将宽度的坐标范围固定在
(−1,1)而将高度的坐标范围调整为(−1.78,1.78)。这个新的范围是根据屏幕的高度与宽度的比例计算出来的,确保了在竖屏模式下,图像的显示比例与屏幕的实际宽高比相匹配。
相反,在横屏模式下,我们可以将高度的坐标范围固定在(−1,1),并将宽度的坐标范围调整为(−1.78,1.78),以适应屏幕的宽度和高度的相对比例。
通过这种方法,无论屏幕是竖屏还是横屏,我们都能保证物体在屏幕上的显示效果是一致的,避免了因屏幕宽高比不同而导致的图像变形。
正交投影
为了适应屏幕方向的变化,我们需要调整坐标空间,这要求我们不再直接使用归一化设备坐标,而是转向一个虚拟坐标空间。在这个虚拟空间中,我们需要一种机制,将虚拟空间的坐标转换回归一化设备坐标,以便OpenGL能够正确渲染它们。这种转换必须考虑屏幕的方向,确保无论在竖屏还是横屏模式下,物体的显示都能保持正确。
我们采用的方法称为正交投影。正交投影的一个特点是,它保持了物体的尺寸不变,无论物体距离观察者有多远或多近。为了更好地理解正交投影的效果,可以想象一个场景,比如从空中俯瞰火车轨道,这些轨道在任何距离下看起来都是平行的,不会随着距离的变化而显得越来越窄。
通过使用正交投影,我们可以确保物体在不同屏幕方向下的显示效果一致,避免了因屏幕方向变化而产生的显示问题,使得物体在竖屏和横屏模式下都能正确显示。
在图形学中,还有一种特殊的正交投影技术,被称为等轴测投影。这种投影技术允许我们从侧面观察物体,从而得到一种独特的视角。等轴测投影常用于城市模拟和策略游戏中,它能够以一种特别的方式重新创建三维空间的视觉效果。
从正交投影坐标回归到归一化坐标
当我们采用正交投影技术将虚拟坐标转换为归一化设备坐标时,实际上是在三维空间中定义了一个可视区域。这个区域里的内容将被显示在屏幕上,而区域外的任何物体都会被系统裁剪掉,不会显示出来。例如,在一个封闭的立方体内,我们可以构建一个简单的场景,而通过正交投影矩阵的作用,这个立方体及其内部的场景将被映射到屏幕上。
在正交投影的视角下,我们可以通过调整投影矩阵来改变立方体在屏幕上的大小,从而控制屏幕上显示的场景范围。此外,我们还可以调整立方体的形状,以适应屏幕的宽高比,确保场景在不同方向的屏幕上都能以正确的比例显示,避免因屏幕比例不同而导致的图像变形。
简而言之,正交投影不仅定义了可视区域,还允许我们通过调整投影矩阵来控制场景的显示范围和形状,以适应不同的屏幕比例。
利用正交投影矩阵改变立方体的天小,以使我们可以在屏幕上看到或多或少的场景。我们也能改变立方体的形状弥补屏幕的宽高比的影响。
线性代数基础
OpenGL大量地使用了向量和矩阵,矩阵的最重要的用途之一就是建立正交和透视投影。其原因之一是,从本质上来说,使用矩阵做投影只涉及对一组数据按顺序执行大量的加法和乘法,这些运算在现代GPU上执行得非常快。
让我们回到你在高中或者大学的时代,复习一下线性代数的基本知识。如果你不记得了、忘得差不多了或者从来没上过这门课,也不需要担心,我们会一起温习这些基础数学。
一旦我们理解了这些基础数学,就可以开始学习如何用矩阵做正交投影。
1.向量
一个向量(vector)是一个有多个元素的一维数组。在OpenGL里,一个位置通常是一个四元素向量,颜色也是一样。我们使用的大多数向量一般都有四个元素。在下面的例子中,我们可以看到一个位置向量,它有一个x、一个y、一个z和一个W分量。
2.矩阵
一个矩阵(Matrix)是一个有多个元素的二维数组。在OpenGL里,我们一般使用矩阵作向量投影,如正交或者透视投影,并且也用它们使物体旋转(rotation)、平移(translation)以及缩放(scaling)。我们把矩阵与每个要变换的向量相乘即可实现这些变换。
下面是一个矩阵的例子。当我们看到一个矩阵如何与一个向量相乘的时候,就会明白那个下标的意义。
2.1矩阵相乘
要让矩阵乘以一个向量,我们把矩阵放在左边,向量放在右边。接下来我们从矩阵的第一行开始,那一行的第一个分量乘以那个向量的第一个分量,那一行的第二个分量乘以那个向量的第二个分量,以此类推;然后,我们把那一行的所有结果相加得出其最终结果的第一个分量。
下面是一个完整的矩阵与向量乘法的例子:
对于第一行,我们让xx和x相乘、xy和y相乘、xz和z相乘以及xw和w相乘,然后把所有四个结果加起来得到这个结果的x分量。
因为我们看到了矩阵下标的作用,它应该显得更有意义了。矩阵第一行的所有四个分量都影响了那个结果x,第二行的所有4个分量都影响了那个结果y,以此类推。在矩阵的每一行内,第一个分量与向量的x相乘,第二个分量也与向量的y相乘,以此类推。
2.2单位矩阵
我们看一个有实际数值的例子。我们从一个简单的矩阵开始,它被称为单位矩阵(identity matrix)。单位矩阵如下图所示:
它之所以被称为单位矩阵,是因为这个矩阵乘以任何向量总是得到与原来相同的向量。就像把任何数字乘以1都会得到原来的数字一样。
例如,用单位矩阵乘以一个包含1、2、3和4的向量:
对于第一行,我们把向量的第一个分量乘以1,并忽略其他与0相乘的分量;对于第二行,同样这么做,但我们保留了这个向量的第二个分量。这样计算的结果证明了这个答案与原来的向量是一模一样的。
让我们简化那些乘法,并把这些结果加在一起。计算结果为:
2.3矩阵平移
既然理解了单位矩阵,让我们看一个非常简单的矩阵类型——平移矩阵(translation matrix),它在OpenGL里十分常用。使用这种类型的矩阵,我们可以把一个物体沿着指定的距离移动。这个矩阵看起来与单位矩阵差不多,但在右侧指定了三个额外的元素。
让我们看一个位置(2,2)的例子,这个位置z的默认值是0,W的默认值是1。我们要把这个向量沿x轴平移3,沿y轴也平移3,因此,把Xtranslation赋值为3,Ytranslation也赋值为3。
结果是这样的:
简化这些乘法之后,我们得到这个:
把这些结果加载一起得到最终的答案:
这个位置正是我们所期望的(5,5)。
这个矩阵之所以可行,是原因它构建在单位矩阵之上,所以,它做的第一件事就复制原来的向量。因为平移分量与W相乘,并且我们通常把一个位置的W分量指定为1(记住,如果我们没有指定w分量,OpenGL会默认把它设为1),这个平移分量只是被加到结果中。
这里要注意的是,W的影响是非常重要的。在下一篇,我们会学习透视投影,在这样投影操作之后,一个坐标的W分量的值可能不是1。如果我们对一个坐标投影以后,其W分量的值不是1了,对其做平移或其他类型的变换时,我们会遇到麻烦,物体会被扭曲。
以上关于向量和矩阵数学的内容帮我们打好了基础:可以继续学习如何定义正交投影了。
使用正交投影
在定义正交投影时,我们将利用Android平台的Matrix类,这个类位于android.opengl包中。Matrix类提供了一个名为orthoM的方法,它能够帮助我们创建一个正交投影矩阵。正如我们在前文中所讨论的,这个正交投影矩阵将被用来调整我们的坐标空间。正交投影矩阵与平移矩阵在结构上非常相似,我们很快就会看到这一点。通过使用orthoM方法,我们可以轻松地实现正交投影,进而控制物体在屏幕上的显示效果。
我们看一下orthoM的所有参数:
orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far)
float m:目标数组,这个数组的长度至少有16个元素,这样它才能存储正交投影矩阵;
int mOffset:结果矩阵起始的偏移值;
float left:x轴的最小范围;
float right: x轴的最大范围;
float bottom:y轴的最小范围;
float top:y轴的最大范围;
float near:z轴的最小范围;
float far:z轴的最大范围。
当我们调用这个方法的时候,它应该产生下面的正交投影矩阵:
不要让这些分数把你吓倒:这与前文中看到的平移矩阵非常相似。这个正交投影矩阵会把所有在左右之间、上下之间和远近之间的事物映射到归一化设备坐标中从-1到1的范围,在这个范围内的所有事物在屏幕上都是可见的。
主要的区别就是z轴有一个负值符号,它的效果是反转z坐标。这就意味着,物体离得越远,z坐标的负值会越来越小。
之所以这样完全是历史和传统的缘故,感兴趣的可以自行了解下左右手坐标系统相关知识。
加入正交投影
接下来让我们开始着手编码。
1.更新着色器
我们需要做的第一件事就是更新着色器,以便它用矩阵变换位置。打开simple_vertexshader.glsl并更新如下:
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main(){
v_Color = a_Color;
gl_Position = u_Matrix * a_Position;
}
我们添加了一个新的uniform定义“u_Matrix”,并把它定义为一个mat4类型,意思是这个uniform代表一个4×4的矩阵。更新位置赋值那一行:
gl_Position = u_Matrix * a_Position;
我们没有传递数组中定义的位置,而是传递那个位置与一个矩阵的乘积。这与我们在前文中讨论的数学运算是一样的。它也意味着顶点数组不用再被翻译为归一化设备坐标了,其将被理解为存在于这个矩阵所定义的虚拟坐标空间中。这个矩阵会把坐标从虚拟坐标空间变换回归一化设备坐标。
2.更新代码
在Renderer添加如下定义:
private val U_MATRIX = "u_Matrix"
它保存顶点着色器中定义的那个新的uniform的名字:
我们还需要一个顶点数组存储那个矩阵:
private val projectionMatrix = FloatArray(16)
我们也需要一个整型值用于保存那个矩阵uniform的位置:
private var uMatrixLocation = 0
2.1创建正交投影矩阵
下一步就是在onSurfaceChanged()中添加如下代码:
val aspectRatio = if(width>height) width.toFloat()/height.toFloat() else height.toFloat()/width.toFloat()
if(width>height){
Matrix.orthoM(projectionMatrix,0,-1f*aspectRatio,aspectRatio,-1f,1f,-1f,1f)
}else{
Matrix.orthoM(projectionMatrix,0,-1f,1f,-1f*aspectRatio,aspectRatio,-1f,1f)
}
这段代码会创建一个正交投影矩阵,这个矩阵会把屏幕的当前方向计算在内。它会建立我们在之前描述的那样的虚拟坐标空间。在Android中不只有一个Matrix类,因此你要确保导入了android.opengl.Matrix。
我们首先计算了宽高比,它使用宽和高中的较大值除以宽和高中的较小值。不管是竖屏还是横屏,这个值都是一样的。
我们接下来调用orthoM(float m, int mOffset, float left, float right, float bottom, float top, float near, float far)。如果在横屏模式下,我们会扩展宽度的坐标空间,这样它的取值范围就不是从-1到1,而是从-aspectRatio到aspectRatio;同时高度的取值范围保持从-1到1。如果在竖屏模式,我们就反过来扩大高度的取值范围,保持宽度的取值范围为从-1到1。
2.2给着色器设置矩阵
在onDrawFrame(),glClear之后添加如下代码:
GLES20.glUniformMatrix4fv(uMatrixLocation,1,false,projectionMatrix,0)
3.运行并查看结果
看起来,好像有点奇怪,跟我们之前的矩形,偏差好大,好像都成正方行了,别急,这是因为我们使用了正交投影导致的,接下来看如何解决。
4.更新矩形顶点坐标数据
让我们更新矩形的结构,让它高点;只更新tableVerticesWithTriangles中的y位置(第二列)来匹配那些数据,如下代码所示:
private var rectangleVertices = floatArrayOf(
0f , 0f , 1f , 1f , 1f ,
-0.5f , -0.8f, 0.3f , 0.3f , 0.3f ,
0.5f , -0.8f, 0.3f , 0.3f , 0.3f ,
0.5f , 0.8f, 0.3f , 0.3f , 0.3f ,
-0.5f , 0.8f, 0.3f , 0.3f , 0.3f ,
-0.5f , -0.8f, 0.3f , 0.3f , 0.3f ,
)
再次运行:
这下它看起来正常了一点。
小结
我们投入了时间来学习线性代数的基础知识,并利用这些知识理解了矩阵与向量相乘的效果。随后,我们掌握了如何定义正交投影矩阵,这个矩阵使我们能够重新设置坐标空间,解决了在屏幕从竖屏旋转到横屏时出现的扭曲问题。
如果你对矩阵数学有任何疑问或不清晰的地方,可能需要回顾本篇中的矩阵部分知识即可。在之后我们将会更加深入地探讨向量和矩阵的应用。