一、让我们看一看安卓
Abstract
Android 手机在移动智能手机市场占据主导地位,甚至超过了苹果的 iPhone。全球 190 多个国家有数亿部手机使用 Android 操作系统。每天都有 100 万新用户开始使用他们的 Android 手机上网、给朋友发电子邮件、下载应用和游戏。事实上,仅在谷歌 Play 商店,每月就有 15 亿次安卓游戏和应用下载。如果你包括其他提供 Android 游戏和应用销售的网站,如亚马逊 Android 应用商店,那么这个数字甚至更高。
Android 手机在移动智能手机市场占据主导地位,甚至超过了苹果的 iPhone。全球 190 多个国家有数亿部手机使用 Android 操作系统。每天都有 100 万新用户开始使用他们的 Android 手机上网、给朋友发电子邮件、下载应用和游戏。事实上,仅在谷歌 Play 商店,每月就有 15 亿次安卓游戏和应用下载。如果你包括其他提供 Android 游戏和应用销售的网站,如亚马逊 Android 应用商店,那么这个数字甚至更高。
在本章中,您将了解 Android 软件开发工具包(SDK)。您将学习如何设置 Android 开发环境。您还将了解这个环境的主要组件,比如 Eclipse。然后,我们将为 Android 创建和部署一个简单的“Hello World”程序,它既有一个虚拟的 Android 模拟器程序,也有一个真实的 Android 设备。
Android 概述
Android 操作系统是一种广泛应用于手机和平板电脑的操作系统。它甚至被用在一个叫做 Ouya 的视频游戏控制台上。Android 手机从需要合同的昂贵手机到不需要任何合同的廉价预付费手机。为 Android 平台开发程序不需要任何开发者费用,不像苹果移动设备需要年费才能在他们的设备上运行你的程序。一部可以使用 OpenGL ES 2.0 开发 3D 游戏的性能良好的预付费非合约 Android 手机可以在Amazon.com
上买到,价格低至 75-100 美元,免运费。
Android SDK 概述
本节讨论 Android SDK。将涵盖开发系统需求和 SDK 的重要部分,如 SDK 管理器、Android 虚拟设备管理器和实际的 Android 仿真器。
Android 软件开发工具包(SDK)要求
Android 开发可以在 Windows PC、Mac OS 机器或 Linux 机器上进行。确切的操作系统要求如下:
操作系统:
- Windows XP (32 位)、Vista (32 位或 64 位)或 Windows 7 (32 位或 64 位)
- Mac OS X 10.5.8 或更高版本(仅限 x86)
- Linux(在 Ubuntu Linux、Lucid Lynx 上测试)
- 需要 GNU C 库(glibc) 2.7 或更高版本。
- 在 Ubuntu Linux 上,需要 8.04 版或更高版本。
- 64 位发行版必须能够运行 32 位应用。
开发 Android 程序还需要安装 Java 开发工具包。Java 开发套件要求是 JDK 6 或更高版本,位于 www.oracle.com/technetwork/java/javase/downloads/index.html
。
如果你用的是 Mac,那么可能已经安装了 Java。
用 Android 开发工具(ADT)插件修改的 Eclipse IDE 程序构成了 Android 开发环境的基础。Eclipse 的要求如下:
- 日食 3.6.2(太阳神)或以上位于
http://eclipse.org
- Eclipse JDT 插件(包含在大多数 Eclipse IDE 包中)
- Eclipse 的 Android 开发工具(ADT)插件位于
http://developer.android.com/tools/sdk/eclipse-adt.html
Notes
最新版本的 ADT 不再支持 Eclipse 3.5 (Galileo)。有关 Android 开发工具的最新信息,请访问 http://developer.android.com/tools/index.html
。
Android SDK 组件概述
Android SDK 的不同组件是 Eclipse 程序、Android SDK 管理器以及 Android 虚拟设备管理器和仿真器。让我们更详细地看一下每一个。
带有 Android 开发工具插件的 Eclipse
您将花费大部分时间处理的 Android SDK 的实际部分是一个名为 Eclipse 的程序,它是通过 ADT 软件插件专门为 Android 定制的。你将输入新的代码,创建新的类,在 Android 模拟器和真实设备上运行程序。在旧的、功能较弱的计算机上,仿真器可能运行得很慢,以至于最好的选择是在实际的 Android 设备上运行程序。因为我们在本书中处理 CPU 密集型 3D 游戏,所以您应该使用实际的 Android 设备来运行示例项目(参见图 1-1 )。
图 1-1。
Eclipse with Android Development Tools plug-ins
Android SDK 管理器
Android SDK 管理器允许您通过其界面下载新的 Android 平台版本和工具。还会显示当前安装的工具和平台版本。比如图 1-2 中,已经安装了 Android 2.2 平台,准备用于开发。这意味着您可以针对该平台编译您的源代码。
图 1-2。
The Android SDK Manager
Android 虚拟设备
Android SDK 还支持虚拟设备仿真器(见图 1-3 )。在许多情况下,你可以在开发系统的软件模拟器上运行你的 Android 程序,而不是在实际的设备上。但是,这最适合非图形密集型应用。因为这本书是关于 3D 游戏的,所以我们不会使用这个软件模拟器,而是一个真正的 Android 设备。Android 虚拟设备管理器允许您创建新的虚拟 Android 设备,编辑现有的 Android 设备,删除现有的设备,以及启动现有的虚拟 Android 设备。图 1-3 表示有一个名为“Android22”的有效虚拟 Android 设备,它模拟 2.2 版本的 Android 操作系统(API Level 8)并模拟 ARM CPU 类型。Android 操作系统的 2.2 版本非常重要,因为它是第一个支持 OpenGL ES 2.0 的版本,我们将在本书中使用它来开发我们的 3D 图形。OpenGL 是允许程序员在 Android 平台上创建 3D 图形的图形系统。它被设计成独立于硬件的。也就是说,OpenGL 图形命令被设计成在许多不同的硬件平台上是相同的,例如 PC、Mac、Android 等。OpenGL 的 OpenGL 2.0 版本是包含可编程顶点和片段着色器的第一个 OpenGL 版本。OpenGL ES 是常规 OpenGL 的子集,包含的功能较少。
图 1-3。
The Android Virtual Device Manager
图 1-4 描绘了实际仿真器启动后的样子。所描述的仿真器是用于 Android 操作系统 2.2 版本的仿真器。
图 1-4。
The actual Android Virtual Device emulator
如何为发展而设置
首先,您需要下载并安装 Java 开发工具包版本 6 或更高版本。Android 开发环境要求将此作为先决条件。在您验证它已经安装并运行之后,您将不得不安装 Android SDK 的主要组件。
最快、最简单的方法是下载位于 http://developer.android.com/sdk/index.html
的 ADT 包,该包位于“其他平台下载”部分。
ADT 包是一个可下载的 zip 文件,其中包含一个特殊版本的 Eclipse,带有 Android 开发工具插件、Android 虚拟设备管理器、SDK 管理器和工具,以及最新的 Android 平台和 Android 模拟器的最新 Android 系统映像。要安装这个 ADT 包,您需要做的就是创建一个新目录并将文件解压缩到其中。您可以使用免费工具如 7-Zip 来解压缩文件。这样做之后,您可以通过执行位于主包目录下的 Eclipse 目录中的eclipse.exe
文件来执行新的 ADT 集成开发环境。
Note
7-Zip 可以在 www.7-zip.org
下载。
Android 开发工具集成开发环境(IDE)概述
Eclipse IDE 由几个重要的部分组成,我将在这里讨论它们。重要的部分是包资源管理器窗口、源代码区域窗口、大纲窗口和消息窗口,包括一个输出程序员指定的调试消息的窗口,称为 LogCat 窗口。还有其他可用的信息窗口,但它们不太重要,不在本节讨论。
包资源管理器
当你开始一个新的 Android 编程项目时,你将为它创建一个新的包。在 Eclipse 中,有一个名为 Package Explorer 的窗口,默认情况下位于左侧。该窗口列出了位于当前工作空间的所有 Android 包。例如,图 1-5 列出了诸如“AndroidHelloWorld”、“AndroidHelloWorldTest”和“ApiDemos”之类的包
图 1-5。
Package Explorer
您还可以展开一个包,以便通过单击包名称旁边的“加号”来访问与该包相关的所有文件。Java 源代码文件位于“src”目录中,与项目相关的资源,如纹理、3D 模型等。,位于“res”(资源的简称)目录中。双击源代码文件或资源文件,在 Eclipse 中查看它。源文件也可以扩展,这样您就可以对该类的变量和函数有一个总体的了解。您可以双击一个变量或函数,在 Eclipse 的 source view 窗口中找到该变量或函数。在图 1-6 中,“AndroidHelloWorldActivity”类中只有一个函数,就是“onCreate”最后,每个 Android 包都有一个AndroidManifest.xml
文件,该文件定义了运行程序所需的权限、程序特定信息(如版本号、程序图标和程序名称)以及运行程序所需的最低 Android 操作系统。
图 1-6。
A closer look into a package
源代码区域
默认情况下,Eclipse 的中间是 Java 源代码显示窗口。每个不同的 Java 源代码或.xml
文件都显示在各自的选项卡中(见图 1-7 )。
图 1-7。
Java source code area
请注意,在最后一个选项卡的末尾,有一个“➤”,后面跟着“4”这意味着有四个隐藏文件没有显示。您可以通过单击“➤4”区域调出完整的文件列表来访问这些文件。以粗体字列出的文件不会显示,您可以通过用鼠标指针高亮显示并左键单击来选择这些文件进行查看(参见图 1-8 )。
图 1-8。
Accessing hidden Java source and .xml
files
概述
Eclipse 中的 Outline 窗口默认位于右侧,它列出了在源代码窗口中选择的类的变量和函数。在 Outline 窗口中点击变量或函数,可以很容易地跳转到源代码窗口中对应的类变量或类函数(见图 1-9 )。
图 1-9。
Outline window in Eclipse
在图 1-9 中,首先列出了类变量或“字段”,然后是类函数。类变量的一些例子是HIGH_SCORES
、m_BackGroundTexture
和m_Dirty
。一些类函数的例子有FindEmptySlot()
、RenderTitle()
和SortHighScoreTable()
。
达尔维克调试监控服务器(DDMS)
带有 ADT 插件的 Eclipse 还提供了一种通过 Dalvik Debug Monitor 服务器或 DDMS 轻松连接实际 Android 硬件的方式。访问 DDMS 的按钮位于 Eclipse IDE 的右上角。点击此按钮将视图切换到 DDMS(见图 1-10 )。
图 1-10。
The DDMS button
在 DDMS 视图中,您可以使用位于视图右侧的文件浏览器选项卡查看 Android 设备上的实际目录和文件。图 1-11 对此进行了说明。
图 1-11。
Exploring files on your Android device
在左侧,如果您有一个通过 USB 端口连接的实际物理 Android 设备,该设备将显示在设备选项卡中,如图 1-12 所示。
图 1-12。
Devices tab on DDMS
另请注意设备窗口右上角的摄像头图标。如果你点击这个按钮,那么你会捕捉到 Android 设备上当前内容的截图(见图 1-13 )。
图 1-13。
Device Screen Capture in DDMS
从这个弹出窗口中,您可以旋转图像并保存图像,如果您愿意的话。在向最终用户推销您的应用时,这是获取促销图片截图的好方法。
日志猫窗口
在 Eclipse IDE 的底部,默认有几个矩形窗口。其中一个更重要的窗口称为 LogCat 窗口,该窗口显示调试和错误信息,这些信息直接来自运行在 Android 设备上的程序,该设备通过 USB 电缆连接到您的计算机(见图 1-14 )。
图 1-14。
LogCat debugging tab
从 Eclipse 启动 SDK 管理器和 AVD 管理器
要从 Eclipse IDE 中启动 Android SDK 管理器和 AVD 管理器,请单击顶部菜单栏中的“Window ”,并使用列表底部附近的菜单项。SDK 管理器使您能够下载新版本的 Android 平台和其他开发工具。AVD 管理器允许您为 Android 设备仿真器创建和管理虚拟 Android 设备(参见图 1-15 )。
图 1-15。
Launching SDK and AVD Managers from Eclipse
实践示例:非 OpenGL ES 文本“Hello World”程序
在这个动手的例子中,我们将创建一个新的 Android 项目,它将输出一个简单的“Hello World”文本字符串。启动 Eclipse IDE。
首先要做的是指定一个工作空间,您将在那里放置这个新项目。从 Eclipse 主菜单中选择 File ➤ SwitchWorkSpace ➤ Other,弹出一个窗口,您可以在其中选择一个目录作为存储新项目的当前工作空间。使用弹出窗口上的 Browse 按钮导航到要用作工作空间的文件夹,然后点击 OK 按钮将该文件夹设置为当前工作空间。
创建新的 Android 项目
要创建一个新的 Android 项目,选择文件➤新建菜单下的“Android 应用项目”(见图 1-16 )。
图 1-16。
Creating a new Android project in Eclipse
这将弹出一个窗口,您可以在其中指定您的应用名称、项目名称、包名称和 SDK 信息(参见图 1-17 )。
图 1-17。
Entering project and SDK info
在“应用名称”编辑框中,输入“RobsHelloWorld”,这是将显示给程序用户的应用的名称。在项目名称编辑框中,输入“RobsHelloWorld”,这是 Eclipse IDE 中显示的项目名称。输入“com.robsexample.robshelloworld”作为与这个新的 Android 项目相关联的包名。此包名必须是唯一的。
对于最低要求的 SDK 选择 Android 2.2 (Froyo),因为这是支持 OpenGL ES 2.0 的最低 Android 平台。对于目标 SDK,选择您预期已经成功测试您的应用的最高 Android 平台 API。在“编译方式”列表框中,选择要为其编译应用的 API 版本。您可以保留主题列表框的默认值。单击“下一步”按钮进入下一个屏幕。
接下来要做的是配置项目。对于本例,只需接受默认值并点击下一步按钮(见图 1-18 )。
图 1-18。
Configuring a new project
在下一个屏幕中,如果您愿意,您可以配置启动器图标。然而,对于这个例子,你可以接受默认值(见图 1-19 )。
图 1-19。
Configure Launcher Icon
单击下一步按钮。下一个屏幕允许您选择想要创建的活动类型。选择空白活动并点击下一步按钮(参见图 1-20 )。
图 1-20。
Select activity type and Create Activity
接受空白活动的默认值。默认的活动名称是“MainActivity”默认布局名称是“activity_main”,默认导航类型是“None”点击完成按钮创建新的 Android 应用(见图 1-21 )。
图 1-21。
Creating a New Blank Activity
在 Eclipse IDE 左侧的 Package Explorer 窗口中,您应该会看到一个名为“RobsHelloWorld”的新条目,这是我们的新示例程序。关键目录是“src”目录,其中存储了 Java 源代码;“libs”目录,存储外部库;以及“res”目录,其中存储了图形、3D 模型和布局等资源。在“res”目录下,“layout”目录存储应用的图形布局规范;“菜单”目录存储应用菜单相关的布局信息;“values”目录存储实际显示的“Hello World”字符串。最后,一个关键文件是AndroidManifest.xml
文件,它包含关于权限的信息和其他应用特定的信息。(见图 1-22“RobsHelloWorld”项目布局。)
图 1-22。
“RobsHelloWorld” Android project
在 Android 模拟器上运行
在模拟器上运行我们的示例之前,我们必须首先设置一个 Android 虚拟设备。从 Eclipse 菜单中选择窗口➤ Android 虚拟设备管理器来启动虚拟设备管理器。单击新建按钮。应该会弹出另一个窗口,标题为“创建新的 Android 虚拟设备(AVD)”。在“AVD 名称:”字段中输入虚拟设备的名称。选择要仿真的设备并作为目标,如图 1-23 所示。接受其余输入的默认值。单击确定按钮。
图 1-23。
Creating a new Android Virtual Device
接下来,我们必须运行我们的示例。如果您是第一次运行此应用,您必须指定如何运行此应用。确保突出显示“RobsHelloWorld”项目。从 Eclipse 主菜单中选择 Run ➤ Run。
当弹出窗口出现时,选择“Android Application”并单击 OK 按钮运行示例。如果您没有通过 USB 电缆连接到计算机的实际 Android 设备,Eclipse 将在 Android 模拟器上运行该程序(参见图 1-24 )。
图 1-24。
Running your “HelloWorld” example
Android 模拟器应该默认启动并运行我们的示例程序。该程序的实际代码将在本章后面显示(见图 1-25 )。
图 1-25。
“RobsHelloWorld” example running on the Android emulator
在实际的 Android 设备上运行
为了在实际的 Android 设备上下载和运行程序,设备必须进入 USB 调试模式。按下菜单键,这是 Android 手机底部最左边的键。点按“设置”按钮,然后点按“应用”按钮和“开发”按钮。单击“USB 调试”选项。完成后,应检查该项目,如图 1-26 所示。
图 1-26。
Setting USB Debugging mode Note
在 Android 4.0 和更新的机型上,USB 调试选项在设置➤开发者选项下。在 Android 4.2 和更新的机型上,开发者选项默认是隐藏的。若要使其可用,请前往“设置”“➤”“关于手机”,然后轻按内部版本号七次。返回上一屏幕,查找开发人员选项。
接下来,您必须在您的开发系统上安装适用于您的 Android 手机型号的 USB 软件驱动程序。尝试先将您的 Android 设备连接到您的电脑,看看它是否会自动安装正确的驱动程序。如果你不能在你的设备上运行这个程序,那么你必须安装制造商提供的设备驱动程序。通常你的手机制造商有一个可以下载驱动程序的网站。完成此操作后,使用 USB 电缆将您的手机连接到您的开发系统,该电缆很可能包含在您的手机中。
现在,您可以开始使用该设备了。从 Eclipse 主菜单中选择 Run ➤ Run。应该会出现一个窗口,您可以在其中选择在实际的 Android 设备或 Android 虚拟设备上运行该程序(参见图 1-27 )。选择硬件设备,然后单击“确定”按钮。
图 1-27。
Choose a device on which to run your program
设备上运行的程序应与图 1-25 所示的相同。按返回键退出程序。
主要源代码
当你在 Android 开发框架内创建一个新的程序时,你实际上是在编码方面创建一个新的活动。你需要做的是从现有的Activity
类中派生出一个新的类,它是标准 Android 代码库的一部分(见清单 1-1)。
清单 1-1。MainActivity.java
源代码为“RobsHelloWorld”的例子
package com.robsexample.robshelloworld;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
public class MainActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
例如,我们的“HelloWorld”程序包含一个新类MainActivity
,它是从Activity
类派生而来的。
创建这个新活动时,会调用onCreate()
函数。它首先通过super.onCreate()
语句调用其父类中的onCreate()
函数。然后,它将活动的视图设置为项目“res/layout”目录中的activity_main.xml
文件中指定的布局。R 类是一个生成的类,位于“gen”目录中,反映了资源或“res”目录中的当前文件(见图 1-2x2)。
OnCreateOptionsMenu()
功能为程序创建选项菜单。菜单规格位于“res/menu”目录下的activity_main.xml
文件中。
图形布局
本例中的图形布局文件.xml
由代码R.layout.activity_main
引用,该代码引用位于本项目“res/layout”目录中的activity_main.xml
文件(见清单 1-2)。)
清单 1-2。“RobsHelloWorld”的图形布局
<RelativeLayout xmlns:android="
http://schemas.android.com/apk/res/android
xmlns:tools="
http://schemas.android.com/tools
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world"
/>
</RelativeLayout>
这种图形布局规范是一种相对布局类型,具有一个 TextView 组件,其中可以显示静态字母数字文本。
代码android:text
设置要显示的文本。
要显示的文本被设置为一个名为“hello_world
”的字符串变量,它位于“res/values”目录下的文件strings.xml
中。
您可以通过删除“@string/
”部分来硬编码一个字符串值,只需将想要显示的文本用引号括起来,例如
android:text="Hello World EveryBODY!!!"
但是,不建议这样做。
您还可以在 Eclipse 中预览和编辑布局,方法是选择布局文件并单击位于文件视图左下方的图形布局选项卡(参见图 1-28 )。)
图 1-28。
Graphical layout preview in Eclipse
实际的“Hello World”数据
最后,显示实际“Hello World”数据的文件是strings.xml
文件,如清单 1-3 所示。
清单 1-3。“Hello World”的数据
<?xml version=``"1.0"``encoding=``"utf-8"
<resources>
<string name=``"app_name"
<string name=``"hello_world"
<string name=``"menu_settings"
</resources>
用于显示文本的关键变量是“hello_world
”,相关的文本数据是“Hello world!”
摘要
在这一章中,我概述了 Android 游戏开发中的关键组件。我首先讨论了 Android 软件开发工具包(SDK)的主要组件。我讨论了 Eclipse IDE、Android SDK 管理器、Android 虚拟设备管理器和实际的 Android 设备仿真器。接下来,我解释了如何设置开发系统来创建和部署 Android 程序。我讨论了 Eclipse IDE 的关键组件,比如项目浏览器窗口、源代码窗口、大纲窗口和 LogCat 窗口。接下来,我带您一步一步地完成了一个实际操作的例子,这个例子涉及到创建一个“Hello World”程序,这个程序既可以在 Android 模拟器上运行,也可以在实际的 Android 设备上运行。最后,我讨论了这个示例“Hello World”程序是如何构造的。
二、Android 中的 Java
Abstract
在这一章中,我将介绍 Android 3D 游戏开发中的 Java 语言组件。我将从 Android 上的基本 Java 语言的简要概述和回顾开始。然后,我将介绍 Android 上所有应用的基本 Java 程序框架。接下来,我将介绍 Android 上专门利用 OpenGL ES 图形的应用的基本 Java 编程框架。最后,我提供了一个 3D Android OpenGL ES 程序的实际例子。
在这一章中,我将介绍 Android 3D 游戏开发中的 Java 语言组件。我将从 Android 上的基本 Java 语言的简要概述和回顾开始。然后,我将介绍 Android 上所有应用的基本 Java 程序框架。接下来,我将介绍 Android 上专门利用 OpenGL ES 图形的应用的基本 Java 编程框架。最后,我提供了一个 3D Android OpenGL ES 程序的实际例子。
Java 语言概述
关于 Java 语言的这一节旨在作为对计算机编程以及面向对象编程有所了解的人的快速入门指南。本节不是 Java 参考手册。它也不打算涵盖 Java 编程语言的所有特性。
Android 的 Java 语言运行在 Java 虚拟机上。这意味着,同一个编译好的 Java Android 程序可以在许多不同的 Android 手机上运行,这些手机具有不同的中央处理器(CPU)类型。这是未来更快处理单元可扩展性方面的一个关键特性,包括那些专门为增强 3D 游戏而设计的处理单元。这样做的代价是速度。Java 程序比用本机语言为 CPU 编译的程序运行得慢,因为 Java 虚拟机必须解释代码,然后在本机处理器上执行。已经为特定本机处理器编译的程序不需要解释,跳过这一步可以节省执行时间。
但是,您可以使用 Android 原生开发工具包或 NDK 为特定的 Android 处理器类型编译 C/C++ 代码。您还可以从 Java 编程框架中调用本机 C/C++ 函数。因此,对于需要本机编译代码速度的关键函数,您可以将这些函数放入使用 NDK 编译的 C/C++ 函数中,并在主程序中从 Java 代码中调用。
Java 注释
Java 注释可以由单行注释和多行注释组成。
- 单行注释以两个斜杠字符(
//
)开始。
// This is a single-line Java comment
- 多行注释以斜杠后跟星号(
/*
)开始,以星号后跟斜杠(*/
)结束。
/*
This is
a multiline
comment
*/
Java 基本数据类型
Java 数据类型本质上可以是数字、字符或布尔值。
- byte:8 位数字,取值范围为-128 到 127,包括-128 和 127
- short:16 位数字,取值范围为-32,768 到 32,767,包括这两个值
- int:32 位数字,取值范围为-2,147,483,648 到 2,147,483,647,包括这两个值
- long:64 位数字,取值范围为-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807,包括这两个值
- float:单精度 32 位 IEEE 754 浮点数
- double:双精度 64 位 IEEE 754 浮点数
- char:单个 16 位 Unicode 字符,范围从
'\u0000'
(或 0)到'\uffff'
(或 65,535,含 65,535) - 布尔型的:值为真或假的
数组
在 Java 中,您可以根据上一节中列出的基本 Java 数据类型创建元素数组。下面的语句创建了一个名为m_ProjectionMatrix
的 16 个 float 类型元素的数组。
float``[] m_ProjectionMatrix =``new float
数据修饰符
数据修饰符允许程序员控制如何访问和存储变量。它们包括以下内容:
- private:私有变量只能从声明它们的类中访问。下面声明
m_ProjectionMatrix
是私有的,只能从它自己的类中访问:
private float``[] m_ProjectionMatrix =``new float
- public:可以从任何类访问公共变量。以下变量是公共的:
public float``[] m_ProjectionMatrix =``new float
- static:声明为 static 的变量只有一个副本与声明它们的类相关联。下面的静态数组被声明为 static,并驻留在 Cube 类中。该数组定义了三维立方体的图形数据。这个 3D 立方体对于 cube 类的所有实例都是相同的,因此将
CubeData
数组设为静态是有意义的。
static float CubeData[] =
{
// x, y, z, u, v nx, ny, nz
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, -1, 1, 1, // front top left
-0.5f, -0.5f, 0.5f, 0.0f, 1.0f, -1, -1, 1, // front bottom left
0.5f, -0.5f, 0.5f, 1.0f, 1.0f, 1, -1, 1, // front bottom right
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 1, 1, 1, // front top right
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1, 1, -1, // back top left
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -1, -1, -1, // back bottom left
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 1, -1, -1, // back bottom right
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 1, 1, -1 // back top right
};
- final:final 修饰符表示变量不会改变。例如,下面声明变量
TAG
的类型为String
,并且是私有的、静态的,不能被改变。
private static final String TAG = "MyActivity";
Java 运算符
在这一节中,我们将讨论算术、一元、条件、按位和位移运算符。
算术运算符
+
加法运算符(也用于String
串联)-
减法运算符*
乘法运算符/
除法运算符%
余数运算符
一元运算符
+
一元加运算符- -对表达式求反
++
将数值增加 1- -将值减 1
!
反转布尔值
条件运算符
&&
条件-与||
条件-或=
赋值运算符==
等于!=
不等于>
大于>=
大于或等于<
小于<=
小于或等于
按位和位移运算符
∼
一元按位补码<<
带符号左移>>
带符号右移>>>
无符号右移&
按位与^
按位异或|
按位异或
Java 流控制语句
if then
声明
if (expression)
{
// execute statements here if expression evaluates to true
}
if then else
声明
if (expression)
{
// execute statements here if expression evaluates to true
}
else
{
// execute statements here if expression evaluates to false
}
switch
声明
switch(expression)
{
case label1:
// Statements to execute if expression evaluates to
// label1:
break;
case label2:
// Statements to execute if expression evaluates to
// label2:
break;
}
while
声明
while (expression)
{
// Statements here execute as long as expression evaluates // to true;
}
for
声明
for (variable counter initialization;
expression;
variable counter increment/decrement)
{
// variable counter initialized when for loop is first
// executed
// Statements here execute as long as expression is true
// counter variable is updated
}
Java 类
Java 是一种面向对象的语言。这意味着您可以派生或扩展现有的类,以形成现有类的新的定制类。派生类将具有父类的所有功能,以及您可能想要添加的任何新功能。
下面的类是其派生的父类的自定义版本,该父类是 Activity 类。
public class MainActivity extends Activity
{
// Body of class
}
包和类
包是 Java 中把以某种方式相关的类和接口组合在一起的一种方式。例如,一个包可以代表一个游戏或其他单个应用。下面是我在本章末尾提到的“Hello Droid”Android 项目的包名称。
package com.robsexample.glhelloworld;
访问包中的类
为了访问位于其他包中的类,您必须使用“import”语句将它们显示出来。例如,为了使用位于 android.opengl.GLSurfaceView 包中的 GLSurfaceView 类,您必须使用以下语句导入它:
import android.opengl.GLSurfaceView;
然后,您可以使用没有完整包名的类定义,例如
private GLSurfaceView m_GLView;
请参考主要的 Android 开发者网站,了解有关 Android 内置类的更多信息,以及在您自己的程序中使用这些类需要指定的确切导入。
Java 接口
Java 接口的目的是为程序员提供一种标准的方法,在派生类的代码中实现接口中的实际功能。一个接口不包含任何实际的代码,只有函数定义。具有实际代码的函数体必须由实现该接口的其他类来定义。实现接口的类的一个很好的例子是 render 类,它用于在 Android 平台上渲染 OpenGL 中的图形。
public class MyGLRenderer implements GLSurfaceView.Renderer
{
// This class implements the functions defined in the
// GLSurfaceView.Renderer interface
// Custom code
private PointLight m_Light;
public PointLight m_PublicLight;
void SetupLights()
{
// Function Body
}
// Other code that implements the interface
}
访问类变量和函数
您可以通过“.
”操作符访问类的变量和函数,就像在 C++ 中一样。请参见以下示例:
MyGLRenderer m_Renderer;
m_Renderer.m_PublicLight = null; // ok
m_Renderer.SetupLights(); // ok
m_Renderer.m_Light = null; // error private member
Java 函数
Java 函数的一般格式与其他语言相同,比如 C/C++。函数标题以可选修饰符开始,如 private、public 或 static。接下来是返回值,如果没有返回值或基本数据类型或类,返回值可以是空的。接下来是函数名和参数列表。
Modifiers Return_value FunctionName(ParameterType1 Parameter1, ...)
{
// Code Body
}
在本章末尾的“Hello Droid”示例中,我们的 Vector3 类中的一个函数示例是:
static Vector3 CrossProduct(Vector3 a, Vector3 b)
{
Vector3 result = new Vector3(0,0,0);
result.x= (a.y*b.z) - (a.z*b.y);
result.y= (a.z*b.x) - (a.x*b.z);
result.z= (a.x*b.y) - (a.y*b.x);
return result;
}
同样在 Java 中,所有作为对象的参数都是通过引用传递的。
调用父函数
使用@Override
注释,派生类中的函数可以覆盖父类或超类中的函数。这不是必需的,但有助于防止编程错误。如果意图是重写父函数,但该函数实际上并没有这样做,那么将会产生一个编译器错误。
为了让派生类中的函数实际调用父类中相应的函数,可以使用超级前缀,如下所示。
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Create a MyGLSurfaceView instance and set it
// as the ContentView for this Activity
m_GLView = new MyGLSurfaceView(this);
setContentView(m_GLView);
}
Note
更多 Java 教程可以在 http://docs.oracle.com/javase/tutorial/
上找到。
基本的 Android Java 程序框架
在这一节中,我将介绍基本的 Android Java 程序框架。这个框架适用于所有 Android 程序,而不仅仅是 Android 3D 游戏或一般的游戏。我从活动生命周期的概述开始。然后,我将介绍生命周期中的关键案例,并通过添加代码来跟进,通过使用 debug 语句,您可以看到活动生命周期中的变化。
Android 活动生命周期概述
Activity 类是 Android 框架中的主要入口,程序员可以在这里创建新的 Android 应用和游戏。为了在这个框架内有效地编码,您必须理解活动类的生命周期。图形流程图样式概述见图 2-1 。
图 2-1。
Activity class callback life cycle
关键活动生命周期案例
在编写活动类时,有一些关键的情况需要考虑。
- 另一个活动来到前台:当前活动暂停;也就是说,活动的
onPause()
函数被调用。 - 电源键关闭:调用当前活动的
onPause()
函数;电源键重新打开;然后调用活动的onResume()
函数,接着返回到活动的恢复。 - 手机方向改变:调用当前活动的
onPause()
函数。调用活动的onStop()
函数。活动的onDestroy()
函数被调用。最后,使用新的方向创建先前活动的新实例,并调用onCreate()
。 - 按下返回键:调用当前活动的
onPause()
函数。调用活动的onStop()
函数。最后,调用活动的onDestroy()
函数。该活动不再有效。 - 按下 Home 键:调用当前活动的
onPause()
函数。调用onStop()
功能,用户被带到主屏幕,在那里可以开始其他活动。如果用户试图通过点击图标开始之前停止的活动,则调用之前活动的onRestart()
函数。接下来,调用onStart()
函数。然后调用onResume()
函数。该活动再次激活并正在运行。
从图 2-1 中得到的重要概念是,无论何时onPause()
被调用,你都应该保存游戏状态。
查看活动生命周期的运行
清单 2-1 展示了这些回调函数在我们在第一章中创建的新 MainActivity 类中的样子。添加到每个回调中的日志语句将错误日志消息输出到 LogCat 窗口,指示正在执行哪个回调。尝试输入额外的代码并运行程序,亲眼看看正在执行的生命周期回调。
清单 2-1。添加了生命周期回调的“RobsHelloWorld”示例
package com.robsexample.robshelloworld;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
public class MainActivity extends Activity {
private static final String TAG = "MyActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e(TAG,"onCreate() called!");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
protected void onStart() {
super.onStart();
Log.e(TAG, "onStart() called!");
}
@Override
protected void onRestart() {
super.onRestart();
Log.e(TAG, "onRestart() called!");
}
@Override
protected void onStop() {
super.onStop();
Log.e(TAG, "onStop() called!");
}
@Override
protected void onResume() {
// Ideally a game should implement onResume() and onPause()
// to take appropriate action when the activity looses focus
super.onResume();
Log.e(TAG, "onResume() called!");
}
@Override
protected void onPause() {
// Ideally a game should implement onResume() and onPause()
// to take appropriate action when the activity looses focus
super.onPause();
Log.e(TAG, "onPause() called!");
}
@Override
protected void onDestroy()
{
// Implement onDestroy() to release objects and free up memory when
// an Activity is terminated.
super.onDestroy();
Log.e(TAG, "onDestroy() called!");
}
}
基本的 Android Java OpenGL 框架
在这一节中,我将介绍基本的 Android Java OpenGL 框架,它是所有 OpenGL 相关应用的基础,包括游戏。我首先用一个单一的 OpenGL 视图介绍了程序的基本框架。接下来,我将介绍一个包含多个视图的框架,这些视图将 OpenGL 视图作为用户界面的一部分。
用于单视图 OpenGL ES 应用的基本 Android OpenGL ES 框架
在这一节中,我将讨论如何创建一个只有一个 OpenGL ES 2.0 视图的 OpenGL ES 2.0 Android 应用。我首先讨论定制的 GLSurfaceView 类。然后,我讨论了我们需要的自定义渲染器来绘制 3D OpenGL ES 对象。
自定义 GLSurfaceView 视图
为了创建你自己的基于 OpenGL ES 的自定义游戏,你必须创建一个自定义GLSurfaceView
,一个绘制这个自定义GLSurfaceView
的自定义渲染器,然后通过你的自定义活动类中的setContentView()
函数将这个新的自定义GLSurfaceView
设置为主视图。
当活动暂停或恢复时,必须通知自定义 GLSurfaceView 对象。这意味着在活动中调用onPause()
或onResume()
时,必须调用 GLSurfaceView 对象中的onPause()
和onResume()
函数。
在下面的自定义 MyGLSurfaceView 类(从 GLSurfaceView 类派生)中,您还必须通过在构造函数内调用 setEGLContextClientVersion(2)来将要使用的 OpenGL ES 版本设置为 2.0。您还必须在构造函数中使用setRenderer(new MyGLRenderer())
语句设置您的自定义渲染器,在下面的示例中是 MyGLRenderer。参见清单 2-2。
清单 2-2。单个 OpenGL ES View 应用的活动类
package robs.demo.robssimplegldemo;
import android.app.Activity;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
public class RobsSimpleOpenGLDemoActivity extends Activity
{
private GLSurfaceView m_GLView;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Create a MyGLSurfaceView instance and set it
// as the ContentView for this Activity
m_GLView = new MyGLSurfaceView(this);
setContentView(m_GLView);
}
@Override
protected void onPause()
{
super.onPause();
m_GLView.onPause();
}
@Override
protected void onResume()
{
super.onResume();
m_GLView.onResume();
}
}
///
class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context) {
super(context);
// Create an OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(new MyGLRenderer());
}
}
自定义渲染器
自定义的 MyGLRenderer 类实现了 GLSurfaceView.Renderer 的接口。这意味着这个类需要实现函数onSurfaceCreated()
、onSurfaceChanged()
和onDrawFrame()
。
当创建 OpenGL 表面或用于 OpenGL ES 渲染的 EGL 上下文丢失时,调用函数onSurfaceCreated()
。在这里创建和初始化你的游戏需要的任何 OpenGL 对象和资源。
每当 OpenGL 表面改变尺寸或创建新表面时,调用 o nSurfaceChanged()
函数。
当需要将 OpenGL 表面渲染到 Android 屏幕时,会调用onDrawFrame()
函数。在这里输入代码来渲染你的 3D 物体。
完整的自定义渲染器类实现见清单 2-3。
清单 2-3。MyGLRenderer 自定义渲染器类
package robs.demo.robssimplegldemo;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
public class MyGLRenderer implements GLSurfaceView.Renderer
{
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config)
{
// Called when an new surface has been created
// Create OpenGL resources here
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height)
{
// Called when new GL Surface has been created or changes size
// Set the OpenglES camera viewport here
}
@Override
public void onDrawFrame(GL10 unused)
{
// Put code to draw 3d objects to screen here
}
}
用于多视图 OpenGL ES 应用的基本 Android OpenGL ES 框架
在本节中,我们将介绍 OpenGL 程序的基本框架,该程序在用户界面或布局中包含多个视图对象,如文本视图、编辑框视图以及 OpenGL 视图。例如,您可以在屏幕的一部分显示编辑框视图,用户可以使用已经内置在软件中的标准虚拟 Android 键盘输入自己的名字,而在屏幕的另一部分运行 OpenGL 动画。
XML 布局文件
以下 XML 布局文件是具有三个视图组件的线性布局:TextView 组件、EditText 组件和名为 MyGLSurfaceView 的自定义 GLSurfaceView 组件。
要在此视图中使用的自定义 GLSurfaceView 类由以下语句指定,该语句是包含其所在的包的类的完整名称:
robs.demo.TestDemoComplete.MyGLSurfaceView
该视图的 id 由以下语句指定:
android:id="@+id/MyGLSurfaceView"
“@
”符号告诉编译器将字符串的其余部分作为身份资源进行解析和扩展。“+
”告诉编译器这个新 id 必须添加到位于 gen/R.java 文件中的资源文件中。“MyGLSurfaceView”是实际的 id(见清单 2-4)。
清单 2-4。多视图 OpenGL ES 应用的 XML 布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:id="@+id/Text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"/>
<EditText
android:id="@+id/EditTextBox1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"/>
<robs.demo.TestDemoComplete.MyGLSurfaceView
android:id="@+id/MyGLSurfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
活动类和 GLSurfaceView 类
前面部分中的 XML 布局被 Activity 类中的语句setContentView()
设置为用户界面。
在 Activity 类中,我们使用findViewById()
函数获取对新创建的 MyGLSurfaceView 对象的引用,因此我们可以在 Activity 类中引用它。
MyGLSurfaceView 类中添加了一个新的构造函数。这是必要的,因为我们在 XML 布局中添加了 MyGLSurfaceView 类(见清单 2-5)。
清单 2-5。多视图 OpenGL ES 活动
package robs.demo.TestDemoComplete;
import android.app.Activity;
import android.os.Bundle;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.MotionEvent;
import android.util.AttributeSet;
public class OpenGLDemoActivity extends Activity
{
private GLSurfaceView m_GLView;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
MyGLSurfaceView V = (MyGLSurfaceView)this.findViewById (R.id.MyGLSurfaceView);
m_GLView = V;
}
@Override
protected void onPause()
{
super.onPause();
m_GLView.onPause();
}
@Override
protected void onResume()
{
super.onResume();
m_GLView.onResume();
}
}
class MyGLSurfaceView extends GLSurfaceView
{
private final MyGLRenderer m_Renderer;
// Constructor that is called when MyGLSurfaceView is created
// from within an Activity with the new statement.
public MyGLSurfaceView(Context context)
{
super(context);
// Create an OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
// Set the Renderer for drawing on the GLSurfaceView
m_Renderer = new MyGLRenderer();
setRenderer(m_Renderer);
}
// Constructor that is called when MyGLSurfaceView is created in the XML
// layout file
public MyGLSurfaceView(Context context, AttributeSet attrs)
{
super(context, attrs);
// Create an OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
// Set the Renderer for drawing on the GLSurfaceView
m_Renderer = new MyGLRenderer();
setRenderer(m_Renderer);
}
}
动手示例:一个 3D OpenGL“Hello Droid”示例
在这个动手练习中,我将介绍一个简单的 3D OpenGL 示例,它为您提供了我将在本书后面介绍的内容的预览。
将项目示例导入 Eclipse
为了运行本书中的项目示例,您需要将它们导入到当前的 Eclipse 工作空间中。在 Eclipse 主菜单下,选择文件➤导入。这应该会打开另一个窗口。选择 Android ➤现有的 Android 代码到工作空间,开始将现有的代码导入到您当前的工作空间。按照下一个窗口中的指示选择一个根目录。选择您想要导入的项目,以及您是否想要将代码复制到现有的工作空间。完成后,单击“完成”按钮。
启动 Eclipse IDE。将第二章项目导入到您当前的工作空间,如果您还没有这样做的话。选择 GLHelloWorld 项目,并在 Eclipse IDE 的 Package Explorer 窗口区域显示源代码列表。
MainActivity 和 MyGLSurfaceView 类
在 Package Explorer 窗口中双击 MainActivity Java 文件,将其显示在源代码区域。该文件定义了新的程序或活动,并遵循前面讨论的单一 OpenGL 视图布局的相同格式(见清单 2-6)。)
清单 2-6。MainActivity 和 MyGLSurfaceView 类
package com.robsexample.glhelloworld;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.opengl.GLSurfaceView;
import android.content.Context;
public class MainActivity extends Activity {
private GLSurfaceView m_GLView;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Create a MyGLSurfaceView instance and set it
// as the ContentView for this Activity
m_GLView = new MyGLSurfaceView(this);
setContentView(m_GLView);
}
@Override
protected void onPause()
{
super.onPause();
m_GLView.onPause();
}
@Override
protected void onResume()
{
super.onResume();
m_GLView.onResume();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
///
class MyGLSurfaceView extends GLSurfaceView
{
public MyGLSurfaceView(Context context)
{
super(context);
// Create an OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(new MyGLRenderer(context));
}
}
MyGLRenderer 类
双击包资源管理器窗口中的 MyGLRenderer 源代码文件,在 Eclipse IDE 源代码窗口区域显示它(参见清单 2-7)。
清单 2-7。MyGLRenderer
package com.robsexample.glhelloworld;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.content.Context;
public class MyGLRenderer implements GLSurfaceView.Renderer
{
private Context m_Context;
private PointLight m_PointLight;
private Camera m_Camera;
private int m_ViewPortWidth;
private int m_ViewPortHeight;
private Cube m_Cube;
public MyGLRenderer(Context context)
{
m_Context = context;
}
void SetupLights()
{
// Set Light Characteristics
Vector3 LightPosition = new Vector3(0,125,125);
float[] AmbientColor = new float [3];
AmbientColor[0] = 0.0f;
AmbientColor[1] = 0.0f;
AmbientColor[2] = 0.0f;
float[] DiffuseColor = new float[3];
DiffuseColor[0] = 1.0f;
DiffuseColor[1] = 1.0f;
DiffuseColor[2] = 1.0f;
float[] SpecularColor = new float[3];
SpecularColor[0] = 1.0f;
SpecularColor[1] = 1.0f;
SpecularColor[2] = 1.0f;
m_PointLight.SetPosition(LightPosition);
m_PointLight.SetAmbientColor(AmbientColor);
m_PointLight.SetDiffuseColor(DiffuseColor);
m_PointLight.SetSpecularColor(SpecularColor);
}
void SetupCamera()
{
// Set Camera View
Vector3 Eye = new Vector3(0,0,8);
Vector3 Center = new Vector3(0,0,-1);
Vector3 Up = new Vector3(0,1,0);
float ratio = (float) m_ViewPortWidth / m_ViewPortHeight;
float Projleft = -ratio;
float Projright = ratio;
float Projbottom = -1;
float Projtop = 1;
float Projnear = 3;
float Projfar = 50; //100;
m_Camera = new Camera(m_Context,
Eye,
Center,
Up,
Projleft, Projright,
Projbottom,Projtop,
Projnear, Projfar);
}
void CreateCube(Context iContext)
{
//Create Cube Shader
Shader Shader = new Shader(iContext, R.raw.vsonelight, R.raw.fsonelight); // ok
//MeshEx(int CoordsPerVertex,
// int MeshVerticesDataPosOffset,
// int MeshVerticesUVOffset,
// int MeshVerticesNormalOffset,
// float[] Vertices,
// short[] DrawOrder
MeshEx CubeMesh = new MeshEx(8,0,3,5,Cube.CubeData, Cube.CubeDrawOrder);
// Create Material for this object
Material Material1 = new Material();
//Material1.SetEmissive(0.0f, 0, 0.25f);
// Create Texture
Texture TexAndroid = new Texture(iContext,R.drawable.ic_launcher);
Texture[] CubeTex = new Texture[1];
CubeTex[0] = TexAndroid;
m_Cube = new Cube(iContext,
CubeMesh,
CubeTex,
Material1,
Shader);
// Set Intial Position and Orientation
Vector3 Axis = new Vector3(0,1,0);
Vector3 Position = new Vector3(0.0f, 0.0f, 0.0f);
Vector3 Scale = new Vector3(1.0f,1.0f,1.0f);
m_Cube.m_Orientation.SetPosition(Position);
m_Cube.m_Orientation.SetRotationAxis(Axis);
m_Cube.m_Orientation.SetScale(Scale);
//m_Cube.m_Orientation.AddRotation(45);
}
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config)
{
m_PointLight = new PointLight(m_Context);
SetupLights();
CreateCube(m_Context);
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height)
{
// Ignore the passed-in GL10 interface, and use the GLES20
// class's static methods instead.
GLES20.glViewport(0, 0, width, height);
m_ViewPortWidth = width;
m_ViewPortHeight = height;
SetupCamera();
}
@Override
public void onDrawFrame(GL10 unused)
{
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
m_Camera.UpdateCamera();
m_Cube.m_Orientation.AddRotation(1);
m_Cube.DrawObject(m_Camera, m_PointLight);
}
}
首先调用onSurfaceCreated()
函数。在这个函数中,创建并初始化了一个新的光源,同时也创建了一个 3D 立方体对象。
接下来,调用onSurfaceChanged()
函数。在这个函数中,摄像机被创建和初始化。定义相机的属性,如位置、方向和相机镜头质量。
在onDrawFrame()
功能中,背景被清除为白色。然后摄像机被更新。接下来,将立方体旋转一度,最后,绘制立方体对象。
类别概述
在本书中,3D 对象的基类是 Object3d 类。其他 3D 对象,如 Cube 类,直接或间接从 Object3d 类派生或扩展。
Object3d 类包含其他关键类,如方向类、MeshEx 类、纹理类、材质类和着色器类。
Orientation 类保存 3D 对象的位置、旋转和缩放数据。
MeshEx 类定义了一种用于表示 3D 对象的 OpenGL 3D 网格。
Texture 类定义了一个由位图图像组成的纹理,它可以应用于 3D 对象。
Material 类定义对象的材质属性,这些属性定义对象的颜色和照明属性。这些属性包括发射、环境光、漫射、镜面反射、镜面反射亮度和 Alpha。
- 发光是指物体本身发出的光。
- 环境光指的是环境光照射时材质反射的颜色。环境光在整个对象上是恒定的,不受灯光位置或观察者位置的影响。
- 漫射属性指的是当受到漫射光照射时,材质反射的颜色。穿过对象的漫射光的强度取决于对象的顶点法线与光方向所成的角度。
- 镜面反射属性是指材质反射的镜面反射颜色。镜面反射的颜色取决于观察者的位置,灯光的位置,以及物体的顶点法线。
- Specular _ Shininess 属性指的是对象上的高光反射的强度。
- Alpha 值是对象的透明度。
Shader 类定义如何绘制和照亮 3D 对象。它由顶点着色器和像素或片段着色器组成。顶点着色器确定对象顶点在 3D 世界中的位置。片段着色器确定被着色对象的颜色。
Camera 类表示进入 OpenGL 3D 世界的视图。位置、方向和相机镜头属性都包含在这个类中。
Cube 类包含顶点位置数据、顶点纹理数据和顶点法线数据,这些数据是用纹理和光照渲染 3D 立方体所需要的。
PointLight 类代表我们的光源。这种光是仿照点光源,如太阳。这种光位于空间中的单个点,光向各个方向辐射。灯光特性包括环境颜色、漫射颜色和镜面颜色。
Vector3 类保存由 x、y 和 z 分量组成的 3D 矢量的数据,以及 3D 矢量数学函数。
我将在后面的章节中更详细地介绍上面提到的类,所以如果您没有完全理解所有的概念,请不要担心。这一章的目的是给你一个包含在本书中的一些关键类的简要概述,并向你展示它们是如何在实际程序中使用的。
实验“你好机器人”
让我们做一些动手实验,摆弄一下灯光。在装有 2.2 版或更高版本操作系统的 Android 手机上运行“GLHelloWorld”程序。图 2-2 显示了默认情况下您应该看到的内容。你应该看到一个 3D 旋转立方体,它的两面都有机器人的纹理。
图 2-2。
Default output
通过注释掉旋转立方体的onDrawFrame()
函数中的语句来停止立方体旋转,如下所示:
//m_Cube.m_Orientation.AddRotation(1);
通过更改语句,将背景颜色更改为黑色
GLES20.``glClearColor
到
GLES20.``glClearColor
它也位于onDrawFrame()
函数中。图 2-3 显示了你现在应该看到的。
图 2-3。
Light positioned in front of and above cube
接下来,让我们改变灯光的位置,使其位于机器人的右侧。我们是向下看负 z 轴,正 x 轴指向右边,负 x 轴指向左边,正 y 轴向上。机器人位于原点,即位置(0,0,0)。将 SetupLights()中的灯光位置更改为以下位置:
Vector3 LightPosition =``new
这将把光线移到机器人的右边。运行程序(参见图 2-4 )。你可以清楚地看到左臂变暗了,因为大部分光线都落在立方体的右侧。
接下来,更改灯光位置,使灯光位于立方体的左侧。
Vector3 LightPosition =``new
运行程序。您应该会看到类似图 2-5 的内容。
接下来,更改灯光位置,使灯光位于立方体上方。将灯光位置更改为以下位置:
Vector3 LightPosition =``new
您应该会看到类似图 2-6 的内容。注意,机器人的腿是深色的。
图 2-6。
Light positioned high above cube
图 2-5。
Light positioned on left side of cube
图 2-4。
Light positioned on right side of cube
接下来,将灯放置在立方体下方远处(见图 2-7 )。
Vector3 LightPosition =``new
图 2-7。
Light positioned far below cube
运行程序。您应该会看到类似于图 2-7 的内容。
在SetupLights()
函数中随意试验更多的光属性。例如,尝试更改“漫反射”、“环境光”和“高光”的值,看看它们对对象会有什么影响。
摘要
在这一章中,我介绍了与 Android 编程相关的 Java 编程语言。首先,我介绍了 Java 的基础知识,比如数据类型、函数、类和操作符。然后我们看了适用于所有 Android 应用的基本 Java 程序框架。接下来,我介绍了专门应用于 OpenGL ES 应用的特定 Java 程序框架。最后,我展示了一个“Hello Droid”项目,它让您预览了本书中其余代码的结构。
三、3D 数学回顾
Abstract
在这一章中,我将介绍向量和矩阵。向量和矩阵是 3D 游戏编程的关键,例如确定 3D 对象在场景中的位置以及如何将 3D 对象投影到 2D 屏幕上。向量也可用于定义速度和力等属性。我从讨论向量和可以用向量执行的操作开始。然后,我将介绍矩阵以及可以用矩阵执行的与 3D 图形相关的基本操作。最后,我将给出一个实际操作的例子,演示如何在 Android 设备上的真实 3D 图形程序中实际使用向量和矩阵。
在这一章中,我将介绍向量和矩阵。向量和矩阵是 3D 游戏编程的关键,例如确定 3D 对象在场景中的位置以及如何将 3D 对象投影到 2D 屏幕上。向量也可用于定义速度和力等属性。我从讨论向量和可以用向量执行的操作开始。然后,我将介绍矩阵以及可以用矩阵执行的与 3D 图形相关的基本操作。最后,我将给出一个实际操作的例子,演示如何在 Android 设备上的真实 3D 图形程序中实际使用向量和矩阵。
向量和向量运算
向量是与 3D 图形相关的基本主题。在这一节,我将介绍什么是向量以及它们的用途。我还介绍了一些重要的向量函数,比如点积和叉积。
什么是向量?
矢量是一个有方向和大小的量。出于本书的目的,向量将是 3D 向量,在 3D 世界中具有 x、y 和 z 方向的分量。向量可以表示位置、速度、方向、对象的旋转轴、对象的局部轴以及作用在对象上的力。在 OpenGL ES 的 Android 上,坐标系由构成地平面的 x、z 轴和表示高度的 y 轴组成(见图 3-1 )。
图 3-1。
3D vectors and the Android OpenGL ES coordinate system
代表位置的向量
一个向量可以代表一个物体在 Android 3D OpenGL ES 世界中的位置。事实上,在第二章的例子中的 Orientation 类中,对象的位置是一个由 Vector3 类表示的 3D 向量。
private Vector3 m_Position;
从图形上看,你可以在图 3-2 中看到一个代表物体在 3D 世界中位置的矢量。
图 3-2。
Vector representing a position
代表方向的向量
向量也可以表示方向。长度或大小为 1 的矢量称为单位矢量(见图 3-3 )。单位向量很重要,因为您可以设置属性,如对象的速度或作用在对象上的力,方法是首先找到您希望对象移动的方向向量的单位向量,然后将该单位向量乘以一个数字。这个数字代表物体速度的大小或你想施加在物体上的力的大小。最终矢量将包含物体的方向或力的方向以及物体的速度或施加到物体上的力的大小。
图 3-3。
Unit vector representing a direction
代表旋转轴的向量
矢量也可以表示物体的旋转轴。旋转轴是物体绕其旋转的线。在第二章的中的 Orientation 类中,变量m_RotationAxis
是物体绕其旋转的局部轴。
private Vector3 m_RotationAxis;
局部旋转轴的图示见图 3-4 。
图 3-4。
Vector representing a rotation axis of an object
代表力的矢量
向量也可以表示力。力有方向和大小,所以很适合用矢量来表示。在图 3-5 中,你看到一个力矢量作用在一个球上。力的方向是负 x 方向。我将在本书后面更深入地讨论作用在 3D 物体上的力。更具体地说,力将在第五章“运动和碰撞”中讨论
图 3-5。
Vector representing force
代表局部轴的向量
向量也可以表示对象的局部轴。图 3-6 显示了一个三维立方体对象的局部 x、y 和 z 轴。局部轴很重要,因为它们定义了对象的方向。也就是说,它们定义了对象的哪一侧被认为是向上的,对象的哪一部分是右侧,以及对象的哪一部分被认为是前面或向前的部分。例如,如果一个 3D 对象表示一个交通工具,如坦克或汽车,那么知道对象的哪一部分是前面就很好了。例如,如果您想要向前移动坦克或汽车,您将需要世界坐标中的前方或向前向量作为下一个位置计算的一部分。Orientation 类将对象的局部轴定义为m_Right
、m_Up
和m_Forward
。
// Local Axes
private Vector3 m_Right;
private Vector3 m_Up;
private Vector3 m_Forward;
图 3-6。
Vectors representing local axes
我们的向量类
根据本书的代码,向量在 Vector3 类中表示(见清单 3-1)。
清单 3-1。Vector3 类
class Vector3
{
public float x;
public float y;
public float z;
// Vector3 constructor
public Vector3(float _x, float _y, float _z)
{
x = _x;
y = _y;
z = _z;
}
}
在 Vector3 类中,向量的 x、y 和 z 分量由浮点数表示。构造函数接受三个表示 3D 向量的浮点值。例如:
Vector3 m_MyVector = new Vector3(1,2,3);
声明一个名为m_MyVector
的新 Vector3 类,用值 x = 1、y = 2 和 z = 3 初始化。
矢量幅度
向量的大小是向量的标量值,是向量的长度。回想一下,标量值是一个没有关联方向的数值。物体的速度可以用矢量来表示,它有方向和速度两个分量。速度是标量分量,通过找到矢量的大小来计算。矢量的大小通过对 x、y 和 z 分量求平方,将它们相加,然后求平方根得到(见图 3-7 )。
图 3-7。
Vector magnitude calculation
在代码中,向量的大小是由 Vector3 类中的Length()
函数计算的,如清单 3-2 所示。
清单 3-2。长度或幅度函数
float Length()
{
return FloatMath.sqrt(x*x + y*y + z*z);
}
向量归一化
向量的规范化意味着向量的长度或大小变为 1,同时保持向量的方向。规范化是设置向量的好方法,例如速度和力。首先,你会在期望的方向上找到一个向量,然后你会把它规格化,把它的长度变成 1。最后,你可以将向量乘以你想赋予它的大小,比如速度或力量。为了标准化一个矢量,你用矢量的长度除矢量的每个分量(见图 3-8 )。
图 3-8。
Normalizing a vector
在代码中,Vector3 类中的Normalize()
函数执行规范化(见清单 3-3)。
清单 3-3。Normalize()
功能
void Normalize()
{
float l = Length();
x = x/l;
y = y/l;
z = z/l;
}
向量加法
矢量可以加在一起产生一个合成矢量,它是所有单个矢量的效果的组合。你可以把向量首尾相连,用图形方式相加。合成矢量 VR 是从起始矢量的尾部到前一个矢量的头部绘制的矢量(见图 3-9 )。
图 3-9。
Adding vectors together
代码方面,Vector3 类中的Add()
函数将两个向量相加,返回结果向量。向量的每个分量 x、y 和 z 加在一起形成合成向量的新分量(见清单 3-4)。
清单 3-4。加法函数将两个向量相加
static Vector3 Add(Vector3 vec1, Vector3 vec2)
{
Vector3 result = new Vector3(0,0,0);
result.x = vec1.x + vec2.x;
result.y = vec1.y + vec2.y;
result.z = vec1.z + vec2.z;
return result;
}
矢乘法
也可以将标量值乘以向量。例如,如果您想要设置一个对象的速度,它是方向和速度的组合,您将找到一个指向所需方向的向量,将该向量规格化以使向量长度为 1,然后将该向量乘以速度。最终的合成矢量 VR 将指向期望的方向,并具有速度的大小值(见图 3-10 )。
图 3-10。
Multiplying a unit vector of length 1 with a scalar value
在代码方面,Vector3 类中的Multiply()
函数将一个标量值乘以向量。向量的每个分量 x、y 和 z 都乘以标量值(见清单 3-5)。
清单 3-5。Multiply
功能
void Multiply(float v)
{
x *= v;
y *= v;
z *= v;
}
向量否定
向量求反意味着将向量乘以–1,也就是将向量的每个分量乘以–1。基本上,矢量的方向是相反的。看看图 3-11 ,看看这是什么样的图形。
图 3-11。
Vector negation
在代码方面,Vector3 类中的Negate()
函数执行求反(见清单 3-6)。
清单 3-6。Negate
功能
void Negate()
{
x = -x;
y = -y;
z = -z;
}
直角三角形
当试图将向量分解成分量时,直角三角形就派上了用场。例如,如果你知道坦克炮弹的速度和炮弹轨迹与地面形成的角度,那么你就可以得到坦克炮弹的水平和垂直速度。水平速度可以用直角三角形邻边的公式来计算。垂直速度可以用三角形对边的公式来计算。我们来复习一下与直角三角形相关的基本三角恒等式,如图 3-12 所示。
图 3-12。
The right triangle
以下是标准三角恒等式的列表,描述了直角三角形各边的长度与图 3-12 中所示的角度θ之间的关系:
- sin(θ)=对边/斜边
- cos(θ)=邻边/斜边
- 相反=斜边* Sin(θ)
- 相邻=斜边* Cos(θ)
向量点积
两个矢量的矢量点积是矢量 A 的幅值乘以矢量 B 的幅值乘以它们之间角度的余弦。点积通常用于计算两个向量之间的角度。点积的一个应用是在广告牌上,一个上面有复杂图像(如一棵树)的 2D 矩形被转向面向摄像机。这是一种实现类似 3D 效果的方法,方法是让图像始终面向摄像机。如果使用复杂的背景图像,例如一棵树,当从不同角度观看时,观众可能不会注意到这是同一幅图像(见图 3-13 )。
图 3-13。
Dot product formula
你也可以用点积求出两个向量之间的角度。两个向量之间的角度是其余弦由向量 A 和向量 B 的点积除以向量 A 的大小,再乘以向量 B 的大小给出的角度(见图 3-14 )。
图 3-14。
Finding angle from dot product
你可以通过归一化两个向量,然后取点积,来简化上面的等式。分母变成 1,角度是矢量 A 和矢量 b 的点积的反余弦。
通过将向量 A 中的每个分量乘以向量 B 中的相应分量,并将结果相加,可以直接从向量中获得点积。诸如
Dot Product = (Ax * Bx) + (Ay * By) + (Az * Bz)
清单 3-7 给出了实现这一点的 Java 代码。
清单 3-7。DotProduct
功能
float DotProduct(Vector3 vec)
{
return (x * vec.x) + (y * vec.y) + (z * vec.z);
}
向量叉积
两个矢量 A 和 B 的叉积是垂直于 A 和 B 的第三个矢量(见图 3-15 )。叉积可用于广告牌等应用,在这些应用中,您需要找到一个旋转轴,并知道代表图像正面的向量和指向您要转向的对象的向量。清单 3-8 中的代码计算了叉积。
图 3-15。
Cross product
清单 3-8。Cross Product
功能
void crossProduct(Vector3 b)
{
Set((y*b.z) - (z*b.y),
(z*b.x) - (x*b.z),
(x*b.y) - (y*b.x));
}
矩阵和矩阵运算
在这一节中,我将介绍矩阵和矩阵运算。我首先讨论矩阵的定义。然后,我将介绍与矩阵数学相关的各种关键主题以及矩阵的关键属性,因为它们与在 Android 移动平台上开发 3D 游戏所需的 3D 计算机图形相关。本节并不打算涵盖矩阵的每个方面,而是一个矩阵和矩阵数学运算的快速入门指南,对 3D 游戏编程至关重要。
什么是矩阵?
矩阵是三维图形的关键。它们用于确定 3D 对象的最终位置、3D 对象的旋转和 3D 对象的缩放等属性。图 3-16 中定义了一个矩阵。矩阵由数字的列和行组成。我们将使用的一般符号是 Amn。下标 m 表示行号,n 表示列号。例如,A23 表示第 2 行第 3 列的数字。
图 3-16。
Definition of a matrix
就代码而言,我们将矩阵表示为 16 个元素的浮点数组。这转化为 4×4 矩阵;即,具有四行四列的矩阵。下面声明了一个 float 类型的 4x 4 矩阵(总共 16 个元素),该矩阵对于它所在的类是私有的:
private float[] m_OrientationMatrix = new float[16];
内置 Android 矩阵类
标准 Android 类库中有一个矩阵类,提供了很多矩阵函数。您可以使用以下 import 语句来访问该类:
import android.opengl.Matrix;
单位矩阵
单位矩阵是一个正方形矩阵,具有相等数量的行和列,对角线上包含 1,其余的值设置为 0。单位矩阵可用于初始化或重置矩阵变量的值。乘以单位矩阵的矩阵返回原始矩阵。这相当于将一个数乘以 1。例如,假设你有一个跟踪物体旋转的矩阵。为了将对象重置回其原始旋转,您需要将矩阵设置为单位矩阵(见图 3-17 )。
图 3-17。
The identity matrix
就代码而言,您可以使用以下语句将矩阵设置为单位矩阵:
//static void setIdentityM(float[] sm, int smOffset)
Matrix.setIdentityM(m_OrientationMatrix, 0);
浮动数组m_OrientationMatrix
中包含的矩阵将被设置为单位矩阵。数组中相对于矩阵数据的起始位置有 0 偏移。
矩阵转置
通过将矩阵的行重写为列来创建矩阵的转置。你将不得不使用矩阵转置来计算法线矩阵的值,该矩阵用于照明(见图 3-18 )。
图 3-18。
Matrix transpose
以下代码语句转置一个 4x 4 矩阵m_NormalMatrixInvert
,并将结果放入m_NormalMatrix
。两个矩阵的数据中的偏移量都是 0。
//static void transposeM(float[] mTrans, int mTransOffset,
// float[] m, int mOffset)
Matrix.transposeM(m_NormalMatrix, 0, m_NormalMatrixInvert, 0);
矩阵乘法
矩阵 A 和矩阵 B 的矩阵乘法是通过将 A 的行中的元素乘以 B 的列中的相应元素并将乘积相加来完成的。矩阵乘法在平移对象、旋转对象、缩放对象和在 2D 屏幕上显示 3D 对象时是必不可少的。比如图 3-19 中,矩阵 A 正在乘以矩阵 B,结果放入矩阵 c。
图 3-19。
Matrix multiplication
C11 = (A11 * B11) + (A12 * B21) + (A13 * B31)
C12 = (A11 * B12) + (A12 * B22) + (A13 * B32)
C21 = (A21 * B11) + (A22 * B21) + (A23 * B31)
C22 = (A21 * B12) + (A22 * B22) + (A23 * B32)
在代码中,使用位于标准 Android 内置 Matrix 类中的multiplyMM()
函数。这个函数将两个 4x 4 矩阵相乘,并将结果存储在第三个 4x 4 矩阵中。以下语句将m_PositionMatrix
乘以m_RotationMatrix
,并将结果放入TempMatrix
。矩阵数据的所有数组偏移量都是 0。
//static void multiplyMM(float[] result, int resultOffset,
// float[] lhs, int lhsOffset,
// float[] rhs, int rhsOffset)
Matrix.multiplyMM(TempMatrix, 0, m_PositionMatrix, 0, m_RotationMatrix, 0);
矩阵求逆
如果矩阵 A 是 n 行 n 列,并且存在另一个也是 n 行 n 列的矩阵 B,使得 AB =单位矩阵,BA =单位矩阵,那么 B 是 A 的逆。这是矩阵逆的定义。
在代码中,您可以使用函数invertM()
来寻找 4x 4 矩阵的逆矩阵。您必须使用矩阵求逆来计算法线矩阵的值,该矩阵用于计算 3D 对象的光照。以下代码反转m_NormalMatrix
并将结果存储在m_NormalMatrixInvert
中。
//static boolean invertM(float[] mInv, int mInvOffset,
// float[] m, int mOffset)
Matrix.invertM(m_NormalMatrixInvert, 0, m_NormalMatrix, 0);
齐次坐标
齐次坐标是射影几何中使用的坐标系。它们指定 3D 世界中的点。齐次坐标很重要,因为它们用于构建发送到顶点着色器的矩阵,以平移、旋转和缩放 3D 对象的顶点。OpenGL 在内部将所有坐标表示为 3D 齐次坐标。我们在本章前面用来指定点的坐标系是欧几里得坐标系中的笛卡尔坐标系。
齐次坐标的一般形式是(x,y,z,w)。齐次坐标中的点可以通过将所有坐标除以 w 坐标来转换为正常的 3D 欧几里得空间坐标。例如,给定齐次坐标(x,y,z,w)中的点,3D 欧氏空间中的点是(x/w,y/w,z/w)。
由(x,y,z)表示的 3D 欧几里得空间中的点可以在齐次空间中由(x,y,z,1)表示。
我们将在第四章“使用 OpenGL ES 2.0 的 3D 图形”中更深入地讨论 OpenGL ES 2.0 顶点和片段着色器
使用矩阵移动对象
矩阵可以用来做很多事情,例如平移对象、旋转对象、缩放对象以及将 3D 对象投影到 2D 屏幕上。用于在 3D 世界中移动对象的矩阵称为平移矩阵。在图 3-20 中,新位置是通过将旧位置转换成齐次坐标,从这个齐次坐标创建一个矩阵,然后乘以平移矩阵来计算的。值 Tx、Ty、Tz 表示在平面上沿 x、y 和 z 方向移动对象的量。使用矩阵乘法来查找新的 x、y 和 z 坐标会产生以下结果:
x' = x + Tx
y' = y + Ty
z' = z + Tz
图 3-20。
Translating an object
在代码方面,我们使用translateM()
函数将输入矩阵转换为 x、y 和 z 值。
例如,以下代码适当地转换了m_PositionMatrix
矩阵:
//static void translateM(float[] m, int mOffset, float x, float y, float z)
//Translates matrix m by x, y, and z in place.
Matrix.translateM(m_PositionMatrix, 0, position.x, position.y, position.z);
使用矩阵旋转对象
矩阵也用于旋转 3D 对象。图 3-21 显示了如何构建绕 x 轴旋转的旋转矩阵的一个例子。
图 3-21。
Rotation matrix
就代码而言,Matrix 类中有一个内置函数,您可以在该函数中围绕任意旋转轴旋转矩阵,旋转角度以度为单位。
rotateM()
函数将矩阵 m 绕轴(x,y,z)旋转角度 a。您还可以指定矩阵数据开始位置的偏移量。
//rotateM(float[] m, int mOffset, float a, float x, float y, float z)
//Rotates matrix m in place by angle a (in degrees) around the axis (x, y, z)
Matrix.rotateM(m_RotationMatrix, 0,
AngleIncrementDegrees,
m_RotationAxis.x,
m_RotationAxis.y,
m_RotationAxis.z);
使用矩阵缩放对象
您也可以使用矩阵来缩放对象。正方形 4x 4 矩阵的对角线包含 x、y 和 z 方向的比例因子(见图 3-22 )。
图 3-22。
Scale matrix
就代码而言,Matrix 类中的scaleM()
函数在 x、y 和 z 方向上缩放一个矩阵。
//static void scaleM(float[] m, int mOffset, float x, float y, float z)
//Scales matrix m in place by sx, sy, and sz
Matrix.scaleM(m_ScaleMatrix, 0, Scale.x, Scale.y, Scale.z);
组合矩阵
通过将矩阵相乘,可以将对象的平移、旋转和缩放效果结合起来。为了在游戏中渲染 3D 物体,我们需要一个关键的组合矩阵,那就是模型矩阵。模型矩阵是平移矩阵、旋转矩阵和缩放矩阵的组合,相乘后形成一个最终矩阵(见图 3-23 )。
图 3-23。
Model matrix
关于矩阵乘法,需要理解的重要一点是乘法的顺序很重要。也就是说,矩阵乘法是不可交换的。因此,AB 不等于 BA。
例如,如果你想围绕一个轴旋转一个对象,然后平移它,你需要在右手边有旋转矩阵,在左手边有平移矩阵。在代码中,如下所示:
// Rotates object around Axis then translates it
// public static void multiplyMM (float[] result, int resultOffset,
// float[] lhs, int lhsOffset,
// float[] rhs, int rhsOffset)
// Matrix A Matrix B
Matrix.multiplyMM(TempMatrix, 0, m_PositionMatrix, 0, m_RotationMatrix, 0);
multiplyMM()
函数将两个矩阵 A 和 B 相乘。就效果而言,首先应用矩阵 B,然后应用矩阵 A。因此,上面的代码首先围绕其旋转轴旋转对象,然后将其平移到一个新位置。
因此,图 3-23 中的模型矩阵被设置为首先缩放一个对象,然后围绕其旋转轴旋转该对象,然后平移该对象。
动手操作示例:在 3D 空间中操纵对象
在这个动手操作的例子中,我们将集中于操纵 3D 对象的位置、旋转和缩放,以演示本章中涉及的向量和矩阵的概念。
这个例子使用了一些像前面提到的Negate()
这样的向量函数。请确保将这些函数,以及您希望试验的其他函数添加到第二章的 Vector3 类中。你也可以在 apress.com
的源代码/下载区找到这个例子的代码。
构建 3D 对象的模型矩阵
Orientation 类保存 3D 对象的位置、旋转和缩放的数据。它还计算对象的模型矩阵,其中包含对象的位置、旋转和缩放信息(参见上图 3-23 )。
模型矩阵在我们的代码中称为m_OrientationMatrix
。m_PositionMatrix
是平移矩阵;m_RotationMatrix
是我们的旋转矩阵;而m_ScaleMatrix
就是我们的规模矩阵。我们也有一个TempMatrix
用于矩阵的临时存储。
SetPositionMatrix()
函数首先通过调用setIdentity()
将矩阵初始化为单位矩阵,然后通过调用作为标准 Android 库一部分的默认矩阵类中的translateM()
来创建转换矩阵,从而创建转换矩阵。
SetScaleMatrix()
函数通过首先将矩阵初始化为单位矩阵,然后从矩阵类库中调用scaleM()
来创建比例矩阵,从而创建比例矩阵。
UpdateOrientation()
函数实际上构建了模型矩阵。
It first creates the translation matrix by calling SetPositionMatrix()
. Next, SetScaleMatrix()
is called to create the Scale Matrix. Then, the final model matrix starts to be built by calling Matrix.multiplyMM()
to multiply the translation matrix by the rotation matrix. Finally, the result matrix from step 3 is multiplied by the scale matrix and then returned to the caller of the function. The net result is that a matrix is created that first scales a 3D object, then rotates it around its axis of rotation, and then finally puts it into the 3D world at a location specified by m_Position
(see Listing 3-9).
清单 3-9。在定向类中构建模型矩阵
// Orientation Matrices
private float[] m_OrientationMatrix = new float[16];
private float[] m_PositionMatrix = new float[16];
private float[] m_RotationMatrix = new float[16];
private float[] m_ScaleMatrix = new float[16];
private float[] TempMatrix = new float[16];
// Set Orientation Matrices
void SetPositionMatrix(Vector3 position)
{
// Build Translation Matrix
Matrix.setIdentityM(m_PositionMatrix, 0);
Matrix.translateM(m_PositionMatrix, 0, position.x, position.y, position.z);
}
void SetScaleMatrix(Vector3 Scale)
{
// Build Scale Matrix
Matrix.setIdentityM(m_ScaleMatrix, 0);
Matrix.scaleM(m_ScaleMatrix, 0, Scale.x, Scale.y, Scale.z);
}
float[] UpdateOrientation()
{
// Build Translation Matrix
SetPositionMatrix(m_Position);
// Build Scale Matrix
SetScaleMatrix(m_Scale);
// Then Rotate object around Axis then translate
Matrix.multiplyMM(TempMatrix, 0, m_PositionMatrix, 0, m_RotationMatrix, 0);
// Scale Object first
Matrix.multiplyMM(m_OrientationMatrix, 0, TempMatrix, 0, m_ScaleMatrix, 0);
return m_OrientationMatrix;
}
将旋转添加到对象
使用第二章中的“Hello Droid”项目,让我们用代码来演示矢量和矩阵如何在 Android 的 OpenGL ES 2.0 上工作。
在 MyGLRenderer 类的onDrawFrame()
函数中,确保
m_Cube.m_Orientation.AddRotation(1)
语句未被注释。这将在每次执行onDrawFrame()
时增加 1 度的旋转,这将是连续的(见清单 3-10)。
清单 3-10。onDrawFrame()
MyGLRenderer 中的函数
@Override
public void onDrawFrame(GL10 unused)
{
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
m_Camera.UpdateCamera();
m_Cube.m_Orientation.AddRotation(1);
m_Cube.DrawObject(m_Camera, m_PointLight);
}
AddRotation()
函数是 Orientation 类的一部分,如清单 3-11 所示。
清单 3-11。AddRotation()
定位类中的功能
void AddRotation(float AngleIncrementDegrees)
{
m_RotationAngle += AngleIncrementDegrees;
//rotateM(float[] m, int mOffset, float a, float x, float y, float z)
//Rotates matrix m in place by angle a (in degrees) around the axis (x, y, z)
Matrix.rotateM(m_RotationMatrix, 0,
AngleIncrementDegrees,
m_RotationAxis.x,
m_RotationAxis.y,
m_RotationAxis.z);
}
这里发生的是对象要旋转的角度被加到变量m_RotationAngle
中,该变量保存对象的当前旋转角度。然后修改旋转矩阵m_RotationMatrix
,以反映新角度增量的增加。运行程序,你会看到立方体旋转,如图 3-24 所示。
图 3-24。
Cube rotating
在 3D 空间中移动对象
现在,我们将引导你沿着 z 轴来回移动立方体。因为 z 轴正对着你,立方体会变大变小。
首先,我们应该停止立方体的旋转。注释掉AddRotation()
函数,如清单 3-12 所示。
接下来,将m_CubePositionDelta
变量添加到 MyGLRenderer 类中。该变量保存每次调用onDrawFrame()
时将应用于立方体的位置变化的方向和幅度。
新代码的关键部分执行实际的位置更新、边界测试和m_CubePositionDelta
变量方向的改变。
该代码执行以下操作:
Gets the current position of the cube. Tests the position to see if it is within the z position 4 to –4. If the cube is outside these boundaries, then the cube’s direction is reversed. That is, the m_CubePositionDelta
vector is negated. The current position vector of the cube is added to the m_CubePositionDelta
vector and then set as the new position of the cube.
添加清单 3-10 中突出显示的新代码并运行程序。
清单 3-12。添加代码以沿 Z 轴移动立方体
private Vector3 m_CubePositionDelta = new Vector3(0.0f,0,0.1f);
@Override
public void onDrawFrame(GL10 unused)
{
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
m_Camera.UpdateCamera();
// Add Rotation to Cube
//m_Cube.m_Orientation.AddRotation(1);
// Add Translation to Cube
Vector3 Position = m_Cube.m_Orientation.GetPosition();
if ((Position.z > 4) || (Position.z < -4))
{
m_CubePositionDelta.Negate();
}
Vector3 NewPosition = Vector3.Add(Position, m_CubePositionDelta);
Position.Set(NewPosition.x, NewPosition.y, NewPosition.z);
m_Cube.DrawObject(m_Camera, m_PointLight);
}
你应该会看到机器人的图像在一个循环中来回移动(见图 3-25 )。
图 3-25。
Translating an object on z axis
缩放对象
在这里,我将涵盖缩放对象。首先在之前添加的m_CubePositionDelta
变量条目下添加以下语句。
private Vector3 m_CubeScale = new Vector3(4,1,1);
m_CubeScale
变量代表在 x、y 和 z 方向缩放对象的数量。在此示例中,立方体在局部 x 轴方向上按正常大小的四倍缩放,在 y 和 z 方向上按正常大小(1)缩放。
以下语句设置立方体的比例。在您添加的前一个代码后,将此输入到onDrawFrame()
函数中。
// Set Scale
m_Cube.m_Orientation.SetScale(m_CubeScale);
运行程序,您应该会看到图 3-26 中的内容。
图 3-26。
Scaling in the x direction
尝试代码。我做了一些修改,改变了背景颜色以及平移的方向(沿对角线来回移动)和缩放(见图 3-27 )。
图 3-27。
Experimenting with the code
看看你能否复制这些变化。
摘要
在这一章中,我讲述了与向量和矩阵相关的 3D 数学基础。我首先介绍了向量和与向量相关的运算,比如加法、乘法、点积和叉积。接下来,我讲述了矩阵和涉及矩阵的运算,比如矩阵乘法,这是 3D 游戏编程的基本要素。最后,我给出了一个实际例子,演示了向量和矩阵在平移、旋转和缩放 3D 对象中的实际应用。