OpenGL ES 高级进阶:坐标系及矩阵变换

今天给你们讲讲OpenGL ES中的坐标系和矩阵变换,OpenGL ES 中的坐标系实际上有不少,在我以前的文章中,由于对应的效果对坐标系的要求不高,所用的坐标其实是跳过的一系列的坐标变换,这点后面会给你们说,而矩阵变换就是将坐标从一个坐标系转换到另外一个坐标系下。android

咱们先来了解一下OpenGL ES中的虚拟摄像机,在OpenGL ES中,有一个虚拟的摄像机,咱们渲染出来的景像实际上就是这个虚拟摄像机所拍摄到的景像,这个虚拟摄像机的效果和咱们真实生活中的摄像机效果更相似,咱们能够经过调整虚拟摄像机的位置、朝向等参数来获得不一样的观察结果,进而获得不一样的渲染画面,举个形象一点的例子,例如咱们平时玩的3D游戏,有至关一部分是用OpenGL ES渲染的,咱们控制角色移动靠近一个物体时,物体就会变大,就像咱们拿着一个摄像机朝一个物体走过去同样,拍摄到的物体就会变大,角色转身时,能看到不一样的画面,就想是咱们拿着一个摄像机朝不一样的方向拍。git

不过这个摄像机最终在OpenGL ES中是以矩阵的形式呈现出来的,咱们先来看一张总体流程图:github

这张图给咱们展现了OpenGL ES 中的坐标系及矩阵变换过程,具体过程是这样:spa

  • 首先OpenGL ES有个世界坐标系,咱们渲染的物体就是在世界坐标系中,咱们的模型须要放到世界坐标系中,那么当咱们还没放的时候,模型就和世界坐标系没有联系,它就还处于本身的坐标系中,咱们叫作模型坐标系、局部空间、局部坐标系,也就是图中的LOCAL SPACE
  • 当咱们把模型放到世界坐标系中,模型就在世界坐标系里有了坐标,也就是原来在LOCAL SPACE中的那些坐标值,变成了世界坐标系中的坐标值,帮助咱们完成这个变换的就是模型矩阵,对应图中的MODEL MATRIX,因而这样咱们就把模型放到了世界坐标系WORLD SPACE
  • 放到世界坐标系后,是否是就肯定了咱们渲染出来看到的样子?尚未,你们能够想像一下,我把一个东西放在世界坐标系的某个地方,我能够从近处看观察它,也能够从远处观察它,还能够从上下左右观察它,甚至还能够倒着观察它,所以还须要肯定咱们观察它的状态。这里实际上就是在肯定虚拟摄像机的摆放,从API的层面上看,咱们只须要设置Camera的位置、朝向的点坐标、以及Camera的上方向向量就能将观察状态定下来,而这些设置最终会转换成OpenGL ES中的视图矩阵,对应图中的VIEW MATRIX
  • 通过View Matrix的变换后,咱们观察它的结果就肯定了,图中是从距离它必定的距离、上往下观察它,这时候的点坐标就来到了视图坐标系下,对应图中的VIEW SPACE
  • 这时候,咱们能看到什么东西,基本已经肯定了,不过还有一步投影变换,这是什么东西?你们想像一下,咱们看到同一个东西,是否是一般都是近大远小?那么如何实现近大远小?就要靠投影变换,OpenGL ES提供正交投影和透视投影,正交投影没有近大远小的效果,无论在什么距离上看,都同样大,透视投影则有近大远小的效果,也是符合咱们实际生活的一种效果,透视投影应用得比较多,可看下面这张经典图:

  • 通过投影变换后,就会转换到裁剪坐标系CLIP SPACE,这一步不只作了投影,也作了裁剪,也就是裁剪出上图中左图的梯形区域和右图中的矩阵区域,不在这个区域中的物体不会在渲染的图面中看到。咱们玩游戏的时候,你们可能会碰到这样的状况,就是人物走到一个物体的近处,若是很靠近这个物体,画面可能会穿进这个物体中,这就是由于物体的一部分超出了近平面,被裁剪掉了。
  • 再下一步是到NDC(设备标准化坐标)坐标系(图中省略了这一步直接到屏幕坐标系了),正如其名,这一步的坐标都是通过标准化的,在可视范围内的坐标值都是在0~1之间,你们会想咱们以前的教程,里面用的坐标是否是都是0~1的?咱们那种写法实际上就是在用NDC坐标直接来渲染,并无通过矩阵变换,所以功能比较简单,还用不上矩阵变换。
  • 最后就到了咱们的屏幕坐标系,这个坐标系你们应该很是熟悉了,android中的各类view里用的坐标就是屏幕坐标。

这有一个初学者可能会误解的点,就是认为OpenGL ES的坐标范围就是0~1,超出0或1就会超出屏幕就看不见,这种理解其实不许确,坐标范围是多少,取决于说的是什么坐标系,咱们在平时作的更可能是2D渲染,经常就是像我以前的教程里写的坐标那样,直接使用0~1的NDC坐标系,不须要矩阵变换,但实际上OpenGL ES的世界坐标系是没有范围的,是负无穷到正无穷,至于某些坐标下的东西是否最后能渲染出来看到,这就取决于前面说的矩阵变换过程,例如将虚拟摄像机对准一个距离999999的物体,而且物体在裁剪区域内,也是能看到的,并非说坐标必定要是0~1。3d

矩阵变换主要仍是用在3D渲染和一些特殊的2D效果上,例如一个偏转变形的2D平面,若是直接设置NDC坐标,出来的效果会有畸变,须要本身进行透视矫正,关于透视矫正,这里先不展开说了。code

接下来咱们来看一下如何在OpenGL ES中使用矩阵变换,首先看模型矩阵,前面提到过,模型矩阵是把坐标从模型的局部坐标系转换到世界坐标系,这个变换不只是位置的变换,还能够有旋转和缩放,例如把一个物体缩小一点、旋转一点后放到世界坐标系中的某个位置上,所以模型矩阵实际上包含的平移、旋转和缩放,它就等于平移矩阵、旋转矩阵和缩放矩阵相乘:orm

val translateMatrix = getIdentity()
val rotateMatrix = getIdentity()
val scaleMatrix = getIdentity()
val modelMatrix = getIdentity()

// 模型矩阵计算
// Calculate the Model matrix
Matrix.translateM(translateMatrix, 0, translateX, translateY, translateZ)
Matrix.rotateM(rotateMatrix, 0, rotateX, 1f, 0f, 0f)
Matrix.rotateM(rotateMatrix, 0, rotateY, 0f, 1f, 0f)
Matrix.rotateM(rotateMatrix, 0, rotateZ, 0f, 0f, 1f)
Matrix.scaleM(scaleMatrix, 0, scaleX, scaleY, scaleZ)
Matrix.multiplyMM(modelMatrix, 0, rotateMatrix, 0, scaleMatrix, 0)
Matrix.multiplyMM(modelMatrix, 0, modelMatrix, 0, translateMatrix, 0)

复制代码

视图矩阵则是对应前面说的虚拟摄像机,它共由虚拟摄像机的位置、朝向的点坐标、以及虚拟摄像机的上方向向量肯定,OpenGL ES提供了方法来获得视图矩阵,咱们只须要给它传递这些参数就好了:cdn

val viewMatrix = getIdentity()
// 视图矩阵计算
// Calculate the View matrix
Matrix.setLookAtM(
    viewMatrix, 
    0, 
    cameraPositionX, cameraPositionY, cameraPositionZ, 
    lookAtX, lookAtY, lookAtZ, 
    cameraUpX, cameraUpY, cameraUpZ
)
复制代码

接下来是投影矩阵,前面提到投影矩阵有正交投影和透视投影两种,本文中使用透视投影,它也是由OpenGL ES提供的方法来获得,所须要的参数为近平面矩阵的上、下、左、右坐标,近平面距离和远平面矩离(这张图中的Left、Right、Bottom、Top标在了远平面上,实际在OpenGL ES中生成透视投影矩阵的方法参数中的left抄下tbottomup指的是近平面):blog

生成透视投影矩阵代码以下:教程

val projectMatrix = getIdentity()
// 透视投影矩阵计算
// Calculate the Project matrix
Matrix.frustumM(
    projectMatrix,
    0,
    nearPlaneLeft, nearPlaneRight, nearPlaneBottom, nearPlaneTop, 
    nearPlane, 
    farPlane
)
复制代码

如今,模型矩阵、视图矩阵和投影矩阵都生成了,下面将这三个矩阵相乘获得最终的变换矩阵(MVP):

val mvpMatrix = getIdentity()
// MVP矩阵计算
// Calculate the MVP matrix
Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0)
Matrix.multiplyMM(mvpMatrix, 0, projectMatrix, 0, mvpMatrix, 0)
复制代码

而后将MVP矩阵传递到Vertex Shader中与顶点相乘进行矩阵变换:

#version 300 es
precision mediump float;
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_textureCoordinate;
layout(location = 2) uniform mat4 u_mvp;
out vec2 v_textureCoordinate;
void main() {
    v_textureCoordinate = a_textureCoordinate;
    gl_Position = u_mvp * a_position;
}"
复制代码

我作一了个demo,能够调节各类参数实时查看效果:

这个demo是渲染一个立方体,立方体每一个面上贴上一个花的纹理,上面这张图是一个初始状态,咱们来看一下初始的参数:

var translateX = 0f         
var translateY = 0f
var translateZ = 0f
var rotateX = 0f
var rotateY = 0f
var rotateZ = 0f
var scaleX = 1f
var scaleY = 1f
var scaleZ = 1f
var cameraPositionX = 0f
var cameraPositionY = 0f
var cameraPositionZ = 5f
var lookAtX = 0f
var lookAtY = 0f
var lookAtZ = 0f
var cameraUpX = 0f
var cameraUpY = 1f
var cameraUpZ = 0f
var nearPlaneLeft = -1f
var nearPlaneRight = 1f
var nearPlaneBottom = -glSurfaceViewHeight.toFloat() / glSurfaceViewWidth
var nearPlaneTop = glSurfaceViewHeight.toFloat() / glSurfaceViewWidth
var nearPlane = 2f
var farPlane = 100f
复制代码

和模型矩阵Model Matrix相关的参数是translateroratescale,这里初始时咱们不对模型进行变换。

和视图矩阵VIEW MATRIX相关的参数是CameraPositionlookAtCameraUp,咱们把虚拟摄像机放在(0, 0, 5)这个位置,而且让它对向(0, 0, 0),而且摄像机的上方向是(0, 1, 0),也就是把摄像正立着。

和投影矩阵PROJECT MATRIX相关的参数是近平面nearPlane上下左右和距离、以及远平面farPlane距离,咱们将近平面左右设为-1和1,而且上下根据GLSurfaceView和尺寸设置,这样是为了避免变形,近平面设置为2,远平面设置为100。

咱们渲染的这个立方体顶点坐标都是-1和1,也就是在原点那里,因此上面的图中咱们看到的效果就是从Z轴上的(0, 0, 5)这个位置正对看向这个立方体,所以只能看到正面。

下面我调节一些参数看看效果:

上面中咱们经过设置模型矩阵的参数将立方体变换到了(3, 4, -5)这个位置,由于咱们的摄像机没动,仍是对着原点看,那么至关于这个立方体在摄像机的右上方,所以摄像机能看到这个立方体的左面和下面。

再继续看:

这个是把立方体旋转了一下,没什么好解释的。

再来看:

这个是旋转加上了缩放,把x坐标变小、y坐标变大了,因此横向的边就短了,竖向的就长了。

再来:

这个是立方体旋转加上再把虚拟摄像机看的点从(0, 0, 0) 变成了(0, 2, 0),所以效果就是立方体就在视野下方了。

再看:

这个是把虚拟摄像机的上方向从(0, 1, 0)变成了(1, 1, 0),也就是原本虚拟摄像机是正立着的,如今变换歪了45度,因此拍摄到的画面也歪了45度。

好了,还有不少种状况,你们能够到demo里玩玩,看看渲染出来的效果是否与本身的理解和预期一致。

这节的内容比较复杂,若是有疑问,欢迎给我留言讨论哈。

代码在我githubOpenGLESPro项目中,本文对应的是SampleMatrixTransform,项目连接:github.com/kenneycode/…

感谢阅读!

相关文章
相关标签/搜索