目录
8.6 代码集成(Integrating Our Changes)
8.6.1 矩阵层级(A Simple Matrix Hierarchy)
8.6.2 修曲棍球桌面(Adding the New Objects to Our Air Hockey Table)
8.6.3 初始化视图矩阵(Initializing the New Matrices)
8.6.4 更新绘制(Updating onDrawFrame)
8.6 代码集成(Integrating Our Changes)
这一章节最困难的部分已经完成,我们学习了如何使用基本的几何体组合成一些复杂的形体,同时相应的着色器也已经更新了。下一步是把前面的修改集成到AirHockeyRenderer中,同时我们还会学习如何通过添加view矩阵以反映OpenGL中一个比较重要的概念Camera。
但是为什么我们还需要添加另外一个矩阵呢?我们刚刚开始曲棍球项目的时候并没有添加任何的矩阵,我们首先使用正交矩阵解决了横竖屏切换时的比例问题,然后我们使用透视投影矩阵以获得一种3D投影的效果,接着我们使用模型矩阵移动场景中的物体;其实view矩阵仅仅只是模型矩阵的一种延伸,使用它的目的也是一样的,但是它会移动场景中的所有物体而不是只针对单一物体。
8.6.1 矩阵层级(A Simple Matrix Hierarchy)
现在复习下将会使用到的三种矩阵,我们将会使用他们把物体绘制到屏幕上:
- 模型矩阵(Model matrix)
模型矩阵用于把物体放置到世界坐标系(world-space coordinates)中,举个例子:我们的球棍或者冰球的中心一开始位于顶点(0, 0, 0)处,假如没有模型矩阵,那么我们的球棍模型或者冰球模型将会停留在原处(即顶点(0, 0, 0)),假如我们想要移动他们,那么我们就需要自己更新每一个坐标顶点;但是假如有模型矩阵的话则情况就不一样了,只需要把模型矩阵与需要移动的物体的顶点坐标相乘即可。比如假如我们需要把冰球的中心移动到点(5, 5)处,那我们需要做的只是准备相应的模型矩阵然后把它与相关顶点相乘即可。
- 视图矩阵(View matrix)
使用视图矩阵的目的与使用模型矩阵的目的一样,但是会把这种效果施加于场景中的所有物体。由于它会影响场景中的所有物体,因此它的功能就类似于相机(camera):移动相机你就可以从不同的角度观察物体。
使用另外一个单独矩阵的一个好处是我们可以提前准备好需要变换的矩阵,举个例子:想像下我们需要旋转场景并移动一个特定的距离,一种方法是首先使用旋转矩阵首先旋转场景,然后再针对场景中的每一个物体使用平移矩阵;虽然刚刚的方法可行,但是更简单的方法是把这些变换保存到一个单独的矩阵中然后再把它应用于场景中的每一个物体。
- 投影矩阵(Projection matrix)
最后是投影矩阵,这个矩阵的目的是构造3D视角,而且仅仅当屏幕方向改变的时候该矩阵才会改变。
现在再来复习下一个顶点是如何从它的原始坐标变换到屏幕上的屏幕坐标的:
- 模型坐标(
)
这是一个位于模型坐标系中的一个坐标顶点;比如我们的曲棍球桌面所包含的顶点坐标就属于模型坐标。
- 世界坐标(
)
这是使用模型矩阵变换后的坐标(使用模型矩阵变换后的坐标就是世界坐标,位于世界坐标系中)
- 视图坐标(
)
这是相对于我们的眼睛或者相机的坐标;我们使用视图矩阵(view matrix)把物体移动到一个与我们的视点相关的视野中。
- 裁剪坐标(
)
这是使用投影矩阵处理过后的坐标,下一步是进行透视分割。
- 规范化设备坐标(
)
这是位于规范化设备坐标系中的坐标;一旦一个坐标位于这种坐标系中,OpenGL将会把它映射到视口上去,然后就你可以在屏幕上看到相应的顶点了。
下面是刚刚的描述的矩阵链:
相关矩阵都按照这种顺序相乘就可以得到正确的结果。
8.6.2 修曲棍球桌面(Adding the New Objects to Our Air Hockey Table)
现在可以在AirHockeyRender加入新的视图矩阵了,同时我们将会使用新的mallet与puck对象,首先增加如下矩阵的定义:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
private final float[] viewMatrix = new float[16];
private final float[] viewProjectionMatrix = new float[16];
private final float[] modelViewProjectionMatrix = new float[16];
我们将会把视图矩阵存储于viewMatrix中,同时另外两个矩阵变量将会存储相应矩阵相乘结果。现在在mallet定义的后面加入如下定义:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
private Puck puck;
下一步是初始化mallet与puck对象,我们将在onSurfaceCreated()方法中使用指定的大小创建他们,代码如下:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
mallet = new Mallet(0.08f, 0.15f, 32);
puck = new Puck(0.06f, 0.02f, 32);
冰球及球棍的半径可以设置成任何值,每一个物体都是使用32个顶点进行构建的。
8.6.3 初始化视图矩阵(Initializing the New Matrices)
下一步是更新onSurfaceChanged() 并且初始化视图矩阵,现在按照代码更新onSurfaceChanged() 方法:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
// Set the OpenGL viewport to fill the entire surface.
glViewport(0, 0, width, height);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width / (float) height, 1f, 10f);
setLookAtM(viewMatrix, 0, 0f, 1.2f, 2.2f, 0f, 0f, 0f, 0f, 1f, 0f);
}
第一部分代码比较常规,首先设置视口然后设置投影矩阵;后面的则是新内容,我们调用方法setLookAtM() 创建了一个特殊的视图矩阵。
setLookAtM(float[] rm, int rmOffset, float eyeX, float eyeY, float eyeZ, float centerX, float centerY,
float centerZ, float upX, float upY, float upZ)
float[] rm | 这是目的数组,该数组的长度应该至少为16以便它可以存储下视图矩阵。 |
int rmOffset | setLookAtM()方法将会从offset处开始向数组rm中写入数据 |
float eyeX, eyeY, eyeZ | 这是视点(眼睛)处的位置,场景中的所有物件看上去就像是从此位置观察一样。 |
float centerX, centerY, centerZ | 这是眼睛观察的朝向,该位置将会是所绘制场景的中心。 |
float upX, upY, upZ | 说到眼睛,那么这就是你的头顶所指向的位置,upY为1意味着你的头顶方向是垂直向上的。 |
我们使用参数 (0, 1.2, 2.2)调用方法setLookAtM(),这意味着视点在x-z平面上方1.2个单位并靠后2.2个单位处;换句话说场景中的物体位于你的下方1.2单位及你前方的2.2单位处;中心点(0, 0, 0) 则意味着你向下看向位于你前方的原点处, (0, 1, 0) 则意味着垂直向上并且场景没有旋转。
8.6.4 更新绘制(Updating onDrawFrame)
在glClear()的后面添加如下代码:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
这句代码将会把投影矩阵与视图矩阵相乘的结果存储于变量viewProjectionMatrix中,把onDrawFrame()剩余的代码更新为如下:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
positionTableInScene();
textureProgram.useProgram();
textureProgram.setUniforms(modelViewProjectionMatrix, texture);
table.bindData(textureProgram);
table.draw();
// Draw the mallets.
positionObjectInScene(0f, mallet.height / 2f, -0.4f);
colorProgram.useProgram();
colorProgram.setUniforms(modelViewProjectionMatrix, 1f, 0f, 0f);
mallet.bindData(colorProgram);
mallet.draw();
positionObjectInScene(0f, mallet.height / 2f, 0.4f);
colorProgram.setUniforms(modelViewProjectionMatrix, 0f, 0f, 1f);
// Note that we don't have to define the object data twice -- we just
// draw the same mallet again but in a different position and with a
// different color.
mallet.draw();
// Draw the puck.
positionObjectInScene(0f, puck.height / 2f, 0f);
colorProgram.setUniforms(modelViewProjectionMatrix, 0.8f, 0.8f, 1f);
puck.bindData(colorProgram);
puck.draw();
这部分代码与上章节中的代码非常相似,但是也有些不同;首先是在绘制形体之前我们调用了方法positionTableInScene() 及方法positionObjectInScene() ,同时还更新了setUniforms()方法的调用,最后是绘制相应的形体。
你注意到了这里使用了相同的球棍数据绘制了两个球棍吗?只要你想要我们可以使用相同的顶点数据绘制几百个物体,我们需要做的只是在绘制物体之前更新模型矩阵以移动物体到不同的位置上。
下一步是定义positionTableInScene(),代码如下:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
private void positionTableInScene() {
// The table is defined in terms of X & Y coordinates, so we rotate it
// 90 degrees to lie flat on the XZ plane.
setIdentityM(modelMatrix, 0);
rotateM(modelMatrix, 0, -90f, 1f, 0f, 0f);
multiplyMM(modelViewProjectionMatrix, 0, viewProjectionMatrix, 0, modelMatrix, 0);
}
由于桌面最开始由x-y坐标定义的,因此这里围绕x轴旋转了90度,注意这里并不像前面一章把桌面平移了一定距离,因为这里我们想保持桌面位于世界坐标系中的 (0, 0, 0) 处,最后由视图矩阵保证桌面位于我们的视野中(可见)。
最后一步是把所有矩阵变换集成到一个矩阵中去,这是通过把矩阵viewProjectionMatrix 与矩阵modelMatrix 相乘并把结果存储于矩阵modelViewProjectionMatrix中来实现的,后面我们会把结果矩阵传递给着色器程序。
下面是方法positionObjectInScene()的定义:
//AirHockeyWithImprovedMallets/src/com/airhockey/android/AirHockeyRenderer.java
private void positionObjectInScene(float x, float y, float z) {
setIdentityM(modelMatrix, 0);
translateM(modelMatrix, 0, x, y, z);
multiplyMM(modelViewProjectionMatrix, 0, viewProjectionMatrix, 0, modelMatrix, 0);
}
由于球棍及冰球都已经定义于x-z平面上,因此这里没有必要进行旋转。这里仅仅只是根据传递进来的参数进行平移以保证物体位于桌面上的正确位置。
现在可以运行程序了,最后没有什么问题的话效果将会如下图不所示,现在球棍及冰球都已经绘制到屏幕上了,但是现在球棍看起来并不那么真实,这个问题我们将会在第13章进行改善。
这一章终于把一个像样的球棍及冰球绘制出来了,下一章节我们将会添加触摸反馈,并使得我们的曲棍球游戏真正可以交互(点击进入下一章)。
最后附上本章源代码(点击下载)