Android Lesson One: Getting Started
这是在Android上使用OpenGL ES2的第一个教程。 在本课中,我们将逐步介绍代码,并了解如何创建OpenGL ES2上下文并绘制到屏幕上。 我们还将了解着色器是什么以及它们如何工作,以及如何使用矩阵将场景转换为您在屏幕上看到的图像。 最后,您需要在清单文件(AndroidManifest.xml)中添加使用OpenGL ES2的说明,以告知Android应用市场你的应用仅对支持的设备可见。
在开始之前,您需要确保在计算机上安装了以下工具:
1.Java环境
2.Android studio开发工具
3.Android真机设备一部
我们将查看下面的所有代码并解释每个部分的作用。 您可以通过创建自己的项目逐段复制代码,也可以在课程的最后下载完整的项目代码。 安装完工具后,在Android Studio中创建一个新的Android项目。 名称无关紧要,但对于本课,我将应用的入口称为LessonOneActivity。
我们来看看代码:
/** GLSurfaceView的一个引用 */
private GLSurfaceView mGLSurfaceView;
GLSurfaceView是一个特殊的视图,它为我们管理OpenGL表面并将其绘制到Android视图系统中。 它还增加了许多功能,使其更易于使用OpenGL,包括但不限于:
GLSurfaceView使得从Android设置和使用OpenGL相对轻松。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mGLSurfaceView = new GLSurfaceView(this);
// 检查系统是否支持 OpenGL ES 2.0.
final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
if (supportsEs2) {
// 请求使用OpenGL ES 2.0兼容的上下文.
mGLSurfaceView.setEGLContextClientVersion(2);
// 将渲染器设置为我们的演示渲染器,定义如下
mGLSurfaceView.setRenderer(new LessonOneRenderer());
} else {
//如果您想同时支持ES 1和ES 2,则可以在此处创建与OpenGL ES 1.x兼容的渲染器。
return;
}
setContentView(mGLSurfaceView);
}
LessonOneActivity的onCreate()方法是创建OpenGL上下文以及一切开始重要部分。在onCreate()中,在调用父类之后做的第一件事是创建我们的GLSurfaceView。然后我们需要弄清楚系统是否支持OpenGL ES2。为此,我们得到一个ActivityManager实例,它允许我们与全局系统状态进行交互。然后我们可以使用它来获取设备配置信息,它将告诉我们设备是否支持OpenGL ES2。
一旦我们知道设备支持OpenGL ES2,我们告诉GLSurfaceView我们想要一个OpenGL ES2兼容表面,然后我们传入一个自定义渲染器。无论何时调整表面或绘制新帧,系统都会调用此渲染器。我们也可以通过传入不同的渲染器来支持OpenGL ES1.x,但是由于API不同,我们需要编写不同的代码。在本课中,我们只关注支持OpenGL ES2。
最后,我们将内容视图设置为GLSurfaceView,它告诉Activity的内容应由我们的OpenGL表面填充。要进入OpenGL,就这么简单!
@Override
protected void onResume() {
super.onResume();
// 当Activity的onResume()方法被调用时,必须调用GLSurfaceView的onResume()方法
mGLSurfaceView.onResume();
}
@Override
protected void onPause() {
super.onPause();
// 当Activity的onPause()方法被调用时,必须调用GLSurfaceView的onPause()方法
mGLSurfaceView.onPause();
}
GLSurfaceView需要我们在Activity的onResume()和onPaused()时调用它的onResume()和onPause()方法。 我们在此处添加调用以完善我们的Activity。
在本节中,我们将开始研究OpenGL ES2的工作原理以及如何开始在屏幕上绘制内容。 在LessonOneActivity中,我们将自定义的GLSurfaceView.Renderer传递给GLSurfaceView。 渲染器有三个重要的方法,系统会自动调用这些方法:
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig eglConfig) {
}
首次创建Surface时会调用此方法。 如果我们丢失Surface上下文并且稍后由系统重新创建它,也将调用此方法。
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
}
只要Surface发生变化,就会调用它; 例如,从纵向切换到横向时。 在创建Surface后也会调用它。
@Override
public void onDrawFrame(GL10 glUnused) {
}
只要是绘制新帧的时候就会调用它。
您可能已经注意到传入的GL10实例称为glUnused。 使用OpenGL ES2绘图时我们不使用它; 相反,我们使用GLES20类的静态方法。 GL10参数仅在那里,因为相同的接口用于OpenGL ES1.x.
在我们的渲染器可以显示任何内容之前,我们需要显示一些内容。 在OpenGL ES2中,我们通过指定数字数组传递内容。 这些数字可以表示位置,颜色或我们需要的任何其他内容。 在这个演示中,我们将显示三个三角形。
// New class members
/** Store our model data in a float buffer. */
private final FloatBuffer mTriangle1Vertices;
private final FloatBuffer mTriangle2Vertices;
private final FloatBuffer mTriangle3Vertices;
/** How many bytes per float. */
private final int mBytesPerFloat = 4;
/**
* Initialize the model data.
*/
public LessonOneRenderer() {
// This triangle is red, green, and blue.
final float[] triangle1VerticesData = {
// X, Y, Z,
// R, G, B, A
-0.5f, -0.25f, 0.0f,
1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.25f, 0.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.559016994f, 0.0f,
0.0f, 1.0f, 0.0f, 1.0f};
...
// Initialize the buffers.
mTriangle1Vertices = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
...
mTriangle1Vertices.put(triangle1VerticesData).position(0);
...
}
那么,这些是什么意思? 如果你曾经使用过OpenGL 1,你可能会习惯这样做:
glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.25f, 0.0f);
glColor3f(1.0f, 0.0f, 0.0f);
...
glEnd();
这些方法在OpenGL ES2中不起作用。我们不是通过一堆方法调用来定义点,而是定义一个数组。 让我们再看看我们的数组:
final float[] triangle1VerticesData = {
// X, Y, Z,
// R, G, B, A
-0.5f, -0.25f, 0.0f,
1.0f, 0.0f, 0.0f, 1.0f,
...
这代表三角形的一个点。 我们设置了前三个数字代表位置(X,Y和Z),后四个数字代表颜色(红色,绿色,蓝色和alpha(透明度))。 您不必太担心如何定义此数组; 请记住,当我们想要在OpenGL ES2中绘制内容时,我们需要以块的形式传递数据,而不是一次传递一个。
// Initialize the buffers.
mTriangle1Vertices = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
...
mTriangle1Vertices.put(triangle1VerticesData).position(0);
我们在Android上使用Java进行编码,但OpenGL ES2的底层实现实际上是用C语言编写的。在我们将数据传递给OpenGL之前,我们需要将其转换为一种它能够理解的形式。 Java和本机系统可能不会以相同的顺序存储它们的字节,因此我们使用一组特殊的缓冲区类并创建一个足够大的ByteBuffer来保存我们的数据,并告诉它使用本机字节顺序存储其数据。 然后我们将它转换为FloatBuffer,以便我们可以使用它来保存浮点数据。 最后,我们将数组复制到缓冲区中。
这个缓冲区的东西可能看起来很混乱(当我第一次遇到它时候也是这样认为!),但请记住,在将数据传递给OpenGL之前,我们需要做一个额外的步骤。 我们的缓冲区现在可以用于将数据传递到OpenGL。
另外,float buffers are slow on Froyo and moderately faster on Gingerbread,所以你可能不希望经常更换它们。
// New class definitions
/**
* Store the view matrix. This can be thought of as our camera. This matrix transforms world space to eye space;
* it positions things relative to our eye.
*/
private float[] mViewMatrix = new float[16];
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
// Set the background clear color to gray.
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
// Position the eye behind the origin.
final float eyeX = 0.0f;
final float eyeY = 0.0f;
final float eyeZ = 1.5f;
// We are looking toward the distance
final float lookX = 0.0f;
final float lookY = 0.0f;
final float lookZ = -5.0f;
// Set our up vector. This is where our head would be pointing were we holding the camera.
final float upX = 0.0f;
final float upY = 1.0f;
final float upZ = 0.0f;
// Set the view matrix. This matrix can be said to represent the camera position.
// NOTE: In OpenGL 1, a ModelView matrix is used, which is a combination of a model and
// view matrix. In OpenGL 2, we can keep track of these matrices separately if we choose.
Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
...
}
另一个“有趣”的主题是矩阵! 无论何时进行3D编程,这些都将成为您最好的朋友,因此您需要很好地了解它们。
当我们的Surface被创建时,我们要做的第一件事就是将清除屏幕的颜色设置为灰色。 alpha部分也已设置为灰色,但我们在本课程中没有进行Alpha混合,因此该值未使用。 我们只需要设置一次清除屏幕的颜色颜色,因为我们以后不会更改它。
我们要做的第二件事是设置我们的视图矩阵。 我们使用了几种不同类型的矩阵,它们都做了一些重要的事情:
你可以找到对矩阵的一个很好的解释SongHo’s OpenGL Tutorials。 我建议你多阅读几次,直到你理解为止; 别担心,我也阅读了好几次才理解它!
在OpenGL 1中,模型矩阵与视图矩阵是结合在一起的。Camera被假设放在了(0,0,0)位置并且面向-Z方向。
我们不需要手工构建这些矩阵。 Android有一个Matrix帮助程序类,可以为我们做繁重的工作。 在这里,我为摄像机创建了一个视图矩阵,它位于原点后面,朝向远处。
final String vertexShader =
"uniform mat4 u_MVPMatrix; \n" // A constant representing the combined model/view/projection matrix.
+ "attribute vec4 a_Position; \n" // Per-vertex position information we will pass in.
+ "attribute vec4 a_Color; \n" // Per-vertex color information we will pass in.
+ "varying vec4 v_Color; \n" // This will be passed into the fragment shader.
+ "void main() \n" // The entry point for our vertex shader.
+ "{ \n"
+ " v_Color = a_Color; \n" // Pass the color through to the fragment shader.
// It will be interpolated across the triangle.
+ " gl_Position = u_MVPMatrix \n" // gl_Position is a special variable used to store the final position.
+ " * a_Position; \n" // Multiply the vertex by the matrix to get the final point in
+ "} \n"; // normalized screen coordinates.
在OpenGL ES2中,我们想要在屏幕上显示的任何内容首先必须通过顶点和片元着色器。好消息是这些着色器并不像它们看起来那么复杂。顶点着色器对每个顶点执行操作,这些操作的结果用于片元着色器,对每个像素执行额外的计算。
每个着色器基本上由输入,输出和程序组成。首先,我们定义一个uniform类型变量,它是一个包含所有变换的组合矩阵。用于将所有顶点投影到屏幕上。然后我们为位置和颜色定义两个attribute类型变量。这些属性将从我们之前定义的缓冲区中读取,并指定每个顶点的位置和颜色。然后我们定义一个varying类型变量,它在三角形上进行插值计算,并将其传递给片元着色器。当它到达片元着色器时,它将为每个像素保存一个插值。
假设我们定义了一个三角形的三个点分别是红色、绿色和蓝色,我们调整它的大小,使其占据屏幕上的10个像素。 当片元着色器运行时,它将为每个像素包含不同的varying类型颜色。 在某一点上, varying 类型颜色可能是红色,也可能是在红色和蓝色之间,还有可能是更紫色的颜色。
除了设置颜色外,我们还告诉OpenGL顶点的最终位置应该在屏幕上的具体位置。 然后我们定义片元着色器:
final String fragmentShader =
"precision mediump float; \n" // Set the default precision to medium. We don't need as high of a
// precision in the fragment shader.
+ "varying vec4 v_Color; \n" // This is the color from the vertex shader interpolated across the
// triangle per fragment.
+ "void main() \n" // The entry point for our fragment shader.
+ "{ \n"
+ " gl_FragColor = v_Color; \n" // Pass the color directly through the pipeline.
+ "} \n";
这是片元着色器,它实际上会将东西显示到屏幕上。 在这个着色器中,我们从顶点着色器中获取varying类型的颜色值,然后直接将其传递给OpenGL。 该点已经按像素插值,因为片元着色器针对将要绘制的每个像素运行。
更多信息请参考OpenGL ES 2 quick reference card。
// Load in the vertex shader.
int vertexShaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
if (vertexShaderHandle != 0)
{
// Pass in the shader source.
GLES20.glShaderSource(vertexShaderHandle, vertexShader);
// Compile the shader.
GLES20.glCompileShader(vertexShaderHandle);
// Get the compilation status.
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(vertexShaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
// If the compilation failed, delete the shader.
if (compileStatus[0] == 0)
{
GLES20.glDeleteShader(vertexShaderHandle);
vertexShaderHandle = 0;
}
}
if (vertexShaderHandle == 0)
{
throw new RuntimeException("Error creating vertex shader.");
}
首先,我们创建着色器对象。如果成功,我们将获得对象的引用。然后我们使用这个引用来传递着色器源代码,然后我们编译它。我们可以从OpenGL获取状态,看看它是否成功编译。如果有错误,我们可以使用GLES20.glGetShaderInfoLog(着色器)找出原因。 我们按照相同的步骤加载片元着色器。
// Create a program object and store the handle to it.
int programHandle = GLES20.glCreateProgram();
if (programHandle != 0)
{
// Bind the vertex shader to the program.
GLES20.glAttachShader(programHandle, vertexShaderHandle);
// Bind the fragment shader to the program.
GLES20.glAttachShader(programHandle, fragmentShaderHandle);
// Bind attributes
GLES20.glBindAttribLocation(programHandle, 0, "a_Position");
GLES20.glBindAttribLocation(programHandle, 1, "a_Color");
// Link the two shaders together into a program.
GLES20.glLinkProgram(programHandle);
// Get the link status.
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
// If the link failed, delete the program.
if (linkStatus[0] == 0)
{
GLES20.glDeleteProgram(programHandle);
programHandle = 0;
}
}
if (programHandle == 0)
{
throw new RuntimeException("Error creating program.");
}
在我们使用顶点和片元着色器之前,我们需要将它们绑定到一个程序中。 这是将顶点着色器的输出与片元着色器的输入相连接的内容。 这也是让我们从程序传递输入并使用着色器绘制形状的原因。
我们创建一个新的程序对象,如果成功,我们就会附加我们的着色器。 我们希望将位置和颜色作为属性传递,因此我们需要绑定这些属性。 然后我们将着色器链接在一起。
//New class members
/** This will be used to pass in the transformation matrix. */
private int mMVPMatrixHandle;
/** This will be used to pass in model position information. */
private int mPositionHandle;
/** This will be used to pass in model color information. */
private int mColorHandle;
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
{
...
// Set program handles. These will later be used to pass in values to the program.
mMVPMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
mPositionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
mColorHandle = GLES20.glGetAttribLocation(programHandle, "a_Color");
// Tell OpenGL to use this program when rendering.
GLES20.glUseProgram(programHandle);
}
在我们成功链接着色器程序之后,我们完成了几项任务,以便我们可以实际使用它。 第一个任务是获取引用,以便我们可以将数据传递到着色器程序中。 然后我们告诉OpenGL在绘图时使用这个着色器程序。 由于我们在本课中只使用了一个着色器程序,因此我们可以将它放在onSurfaceCreated()而不是onDrawFrame()中。
// New class members
/** Store the projection matrix. This is used to project the scene onto a 2D viewport. */
private float[] mProjectionMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height)
{
// Set the OpenGL viewport to the same size as the surface.
GLES20.glViewport(0, 0, width, height);
// Create a new perspective projection matrix. The height will stay the same
// while the width will vary as per aspect ratio.
final float ratio = (float) width / height;
final float left = -ratio;
final float right = ratio;
final float bottom = -1.0f;
final float top = 1.0f;
final float near = 1.0f;
final float far = 10.0f;
Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
}
onSurfaceChanged()被调用至少一次,并且每当我们的surface被改变时。 由于我们只需要在我们投影到的屏幕发生变化时重置投影矩阵,onSurfaceChanged()就是理想的选择。
// New class members
/**
* Store the model matrix. This matrix is used to move models from object space (where each model can be thought
* of being located at the center of the universe) to world space.
*/
private float[] mModelMatrix = new float[16];
@Override
public void onDrawFrame(GL10 glUnused)
{
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
// Do a complete rotation every 10 seconds.
long time = SystemClock.uptimeMillis() % 10000L;
float angleInDegrees = (360.0f / 10000.0f) * ((int) time);
// Draw the triangle facing straight on.
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.rotateM(mModelMatrix, 0, angleInDegrees, 0.0f, 0.0f, 1.0f);
drawTriangle(mTriangle1Vertices);
...
}
这是实际显示在屏幕上的内容。 首先清除了屏幕,因此我们没有得到任何奇怪的镜面效果。我们希望三角形在屏幕上能有平滑的动画,我们使用时间旋转三角形。 每当您在屏幕上制作动画时,通常最好使用时间而不是帧速率。
实际绘图在drawTriangle中完成:
// New class members
/** Allocate storage for the final combined matrix. This will be passed into the shader program. */
private float[] mMVPMatrix = new float[16];
/** How many elements per vertex. */
private final int mStrideBytes = 7 * mBytesPerFloat;
/** Offset of the position data. */
private final int mPositionOffset = 0;
/** Size of the position data in elements. */
private final int mPositionDataSize = 3;
/** Offset of the color data. */
private final int mColorOffset = 3;
/** Size of the color data in elements. */
private final int mColorDataSize = 4;
/**
* Draws a triangle from the given vertex data.
*
* @param aTriangleBuffer The buffer containing the vertex data.
*/
private void drawTriangle(final FloatBuffer aTriangleBuffer)
{
// Pass in the position information
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Pass in the color information
aTriangleBuffer.position(mColorOffset);
GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mColorHandle);
// This multiplies the view matrix by the model matrix, and stores the result in the MVP matrix
// (which currently contains model * view).
Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
// This multiplies the modelview matrix by the projection matrix, and stores the result in the MVP matrix
// (which now contains model * view * projection).
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
你还记得我们最初创建渲染器时定义的那些缓冲区吗? 我们终于可以使用它们了。 我们需要告诉OpenGL如何使用GLES20.glVertexAttribPointer()来使用这些数据。 让我们来看看第一个调用。
// Pass in the position information
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
我们将缓冲区位置设置为位置偏移量,它位于缓冲区的开头。然后我们告诉OpenGL使用这些数据并将其提供给顶点着色器并将其应用于我们的position属性。我们还需要告诉OpenGL每个顶点或步幅之间有多少个元素。
注意:步幅需要以字节为单位进行定义。虽然我们在顶点之间有7个元素(3个用于位置,4个用于颜色),但实际上我们有28个字节,因为每个浮点数占用4个字节。忘记此步骤可能不会导致任何错误,但您会想知道为什么您在屏幕上看不到任何内容。
最后,我们启用顶点属性并转到下一个属性进行设置。继续看代码,我们构建一个组合矩阵,将点投影到屏幕上。 我们也可以在顶点着色器中执行此操作,但由于它只需要完成一次,所以我们可以只缓存结果。 我们使用GLES20.glUniformMatrix4fv()将最终矩阵传递给顶点着色器,GLES20.glDrawArrays()将我们的点转换为三角形并将其绘制在屏幕上。
呼! 这是一个很重要的课程,如果你完成了这一课,你会非常开心。 我们学习了如何创建OpenGL上下文,传递形状数据,加载顶点和像素着色器,设置转换矩阵,最后将它们组合在一起。 如果一切顺利,您应该会看到类似于下侧屏幕截图的内容。
这一课有很多要消化的内容,你可能需要多次阅读这些步骤才能理解它。 OpenGL ES2需要更多的设置工作才能开始,但是一旦你完成了这个过程几次,你就会记住前面的流程。
在开发应用程序时,我们不希望无法运行这些应用程序的人在市场上看到它们,否则当应用程序在其设备上崩溃时,我们可能会收到大量糟糕的评论和评分。 要防止OpenGL ES2应用程序出现在不支持它的设备上,您可以将其添加到清单中:
这告诉市场您的应用程序需要OpenGL ES2,它会将您的应用程序隐藏掉如果设备不支持它。
尝试更改动画速度,顶点或颜色,看看会发生什么!
可以从GitHub上的项目站点下载本课程的完整源代码。