当今市场上几乎所有的Android手机都具有图形处理单元,简称GPU。 顾名思义,这是专用于处理通常与3D图形相关的计算的硬件单元。 作为应用程序开发人员,您可以利用GPU来创建以很高的帧速率运行的复杂图形和动画。
当前,您可以使用两种不同的API与Android设备的GPU进行交互: Vulkan和OpenGL ES 。 虽然Vulkan仅在运行Android 7.0或更高版本的设备上可用,但所有Android版本都支持OpenGL ES。
在本教程中,我将帮助您开始在Android应用程序中使用OpenGL ES 2.0。
先决条件
要遵循本教程,您需要:
- 最新版本的Android Studio
- 支持OpenGL ES 2.0或更高版本的Android设备
- 最新版本的Blender或任何其他3D建模软件
1.什么是OpenGL ES?
OpenGL,是Open Graphics Library的简称,是一种独立于平台的API,可让您创建硬件加速的3D图形。 OpenGL ES是嵌入式系统OpenGL的缩写,是API的子集。
OpenGL ES是一个非常底层的API。 换句话说,它不提供任何允许您快速创建或操纵3D对象的方法。 相反,在使用它时,您应该手动管理任务,例如创建3D对象的各个顶点和面,计算各种3D变换以及创建不同类型的着色器。
还值得一提的是,Android SDK和NDK一起使您可以用Java和C编写与OpenGL ES相关的代码。
2.项目设置
由于OpenGL ES API是Android框架的一部分,因此您不必向项目中添加任何依赖项就可以使用它们。 但是,在本教程中,我们将使用Apache Commons IO库读取一些文本文件的内容。 因此,将其添加为应用模块的build.gradle文件中的compile
依赖项 :
compile 'commons-io:commons-io:2.5'
此外,为了阻止没有支持您所需的OpenGL ES版本的设备的Google Play用户安装应用,请在项目的清单文件中添加以下<uses-feature>
标签:
<uses-feature android:glEsVersion="0x00020000"
android:required="true" />
3.创建一个画布
Android框架提供了两个可充当3D图形画布的小部件: GLSurfaceView
和TextureView
。 大多数开发人员更喜欢使用GLSurfaceView
,并且仅在打算将其3D图形叠加在另一个View
小部件上时才选择TextureView
。 对于我们将在本教程中创建的应用程序, GLSurfaceView
就足够了。
将GLSurfaceView
小部件添加到布局文件与添加任何其他小部件没有什么不同。
<android.opengl.GLSurfaceView
android:layout_width="300dp"
android:layout_height="300dp"
android:id="@+id/my_surface_view"
/>
请注意,我们已经使小部件的宽度等于其高度。 这样做很重要,因为OpenGL ES坐标系是一个正方形。 如果必须使用矩形画布,请在计算投影矩阵时切记包括其纵横比。 您将在后续步骤中了解什么是投影矩阵。
在Activity
类中初始化GLSurfaceView
小部件就像调用findViewById()
方法并将其ID传递给它一样简单。
mySurfaceView = (GLSurfaceView)findViewById(R.id.my_surface_view);
此外,我们必须调用setEGLContextClientVersion()
方法来显式指定我们将用于在小部件内部绘制的OpenGL ES的版本。
mySurfaceView.setEGLContextClientVersion(2);
4.创建一个3D对象
尽管可以通过手动编码所有顶点的X,Y和Z坐标来用Java创建3D对象,但这非常麻烦。 相反,使用3D建模工具要容易得多。 搅拌器就是这样一种工具。 它是开源的,功能强大的并且非常易于学习。
启动Blender,然后按X删除默认多维数据集。 接下来,按Shift-A并选择“ 网格”>“圆环” 。 现在,我们有了一个由576个顶点组成的相当复杂的3D对象。
为了能够在我们的Android应用程序中使用圆环,我们必须将其导出为Wavefront OBJ文件。 因此,转到文件>导出> Wavefront(.obj) 。 在下一个屏幕中,给OBJ文件命名,确保选中了Triangulate Faces和Keep Vertex Order选项,然后按Export OBJ按钮。
现在,您可以关闭Blender并将OBJ文件移动到Android Studio项目的资产文件夹中。
5.解析OBJ文件
如果您尚未注意到,我们在上一步中创建的OBJ文件是一个文本文件,可以使用任何文本编辑器打开该文件。
在文件中,以“ v”开头的每一行代表一个顶点。 类似地,以“ f”开头的每条线代表一个三角形的面。 每个顶点线包含一个顶点的X,Y和Z坐标,而每个面线则包含三个顶点的索引,这三个顶点共同构成一个面。 这就是您解析OBJ文件所需的全部知识。
在开始之前,创建一个名为Torus的新Java类,并添加两个List
对象,一个用于顶点,一个用于面,作为其成员变量。
public class Torus {
private List<String> verticesList;
private List<String> facesList;
public Torus(Context context) {
verticesList = new ArrayList<>();
facesList = new ArrayList<>();
// More code goes here
}
}
读取OBJ文件的所有各行的最简单方法是使用Scanner
类及其nextLine()
方法。 在循环浏览各行并填充两个列表时,可以使用String
类的startsWith()
方法检查当前行是以“ v”还是“ f”开头。
// Open the OBJ file with a Scanner
Scanner scanner = new Scanner(context.getAssets().open("torus.obj"));
// Loop through all its lines
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
if(line.startsWith("v ")) {
// Add vertex line to list of vertices
verticesList.add(line);
} else if(line.startsWith("f ")) {
// Add face line to faces list
facesList.add(line);
}
}
// Close the scanner
scanner.close();
6.创建缓冲区对象
您无法将顶点和面列表直接传递给OpenGL ES API中可用的方法。 您必须首先将它们转换为缓冲区对象。 要存储顶点坐标数据,我们需要一个FloatBuffer
对象。 对于仅由顶点索引组成的人脸数据,一个ShortBuffer
对象就足够了。
因此,将以下成员变量添加到Torus
类:
private FloatBuffer verticesBuffer;
private ShortBuffer facesBuffer;
要初始化缓冲区,我们必须首先使用allocateDirect()
方法创建一个ByteBuffer
对象。 对于顶点缓冲区,为每个坐标分配四个字节,坐标为浮点数。 创建ByteBuffer
对象后,可以通过调用其asFloatBuffer()
方法将其转换为FloatBuffer
。
// Create buffer for vertices
ByteBuffer buffer1 = ByteBuffer.allocateDirect(verticesList.size() * 3 * 4);
buffer1.order(ByteOrder.nativeOrder());
verticesBuffer = buffer1.asFloatBuffer();
同样,为faces缓冲区创建另一个ByteBuffer
对象。 这次,为每个顶点索引分配两个字节,因为索引是unsigned short
文字。 另外,请确保使用asShortBuffer()
方法将ByteBuffer
对象转换为ShortBuffer
。
// Create buffer for faces
ByteBuffer buffer2 = ByteBuffer.allocateDirect(facesList.size() * 3 * 2);
buffer2.order(ByteOrder.nativeOrder());
facesBuffer = buffer2.asShortBuffer();
填充缓冲器涉及通过的内容循环顶点verticesList
,从每个项目中提取X,Y和Z坐标,并调用put()
方法把缓冲器内的数据。 由于verticesList
仅包含字符串,因此必须使用parseFloat()
将坐标从字符串转换为float
值。
for(String vertex: verticesList) {
String coords[] = vertex.split(" "); // Split by space
float x = Float.parseFloat(coords[1]);
float y = Float.parseFloat(coords[2]);
float z = Float.parseFloat(coords[3]);
verticesBuffer.put(x);
verticesBuffer.put(y);
verticesBuffer.put(z);
}
verticesBuffer.position(0);
请注意,在上面的代码中,我们使用了position()
方法来重置缓冲区的位置。
填充面部缓冲区略有不同。 您必须使用parseShort()
方法将每个顶点索引转换为短值。 此外,由于索引从1开始而不是0,因此必须将索引减去1,然后再将其放入缓冲区。
for(String face: facesList) {
String vertexIndices[] = face.split(" ");
short vertex1 = Short.parseShort(vertexIndices[1]);
short vertex2 = Short.parseShort(vertexIndices[2]);
short vertex3 = Short.parseShort(vertexIndices[3]);
facesBuffer.put((short)(vertex1 - 1));
facesBuffer.put((short)(vertex2 - 1));
facesBuffer.put((short)(vertex3 - 1));
}
facesBuffer.position(0);
7.创建明暗器
为了能够渲染我们的3D对象,我们必须为其创建一个顶点着色器和一个片段着色器。 现在,您可以将着色器视为一个非常简单的程序,该程序使用类似于C的语言称为OpenGL Shading Language(简称GLSL)编写。
您可能已经猜到过,顶点着色器负责处理3D对象的顶点。 片段着色器(也称为像素着色器)负责为3D对象的像素着色。
第1步:创建顶点着色器
在项目的res / raw文件夹中创建一个名为vertex_shader.txt的新文件。
顶点着色器必须在其中具有attribute
全局变量,以便从Java代码接收顶点位置数据。 此外,添加uniform
全局变量以从Java代码接收视图投影矩阵。
在顶点着色器的main()
函数内部,必须设置gl_position
的值, gl_position
是GLSL内置变量,用于确定顶点的最终位置。 现在,您只需将其值设置为uniform
变量和attribute
全局变量的乘积即可。
因此,将以下代码添加到文件中:
attribute vec4 position;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
}
步骤2:创建片段着色器
在项目的res / raw文件夹中创建一个名为fragment_shader.txt的新文件。
为了使本教程简短,我们现在将创建一个非常简约的片段着色器,该着色器仅将橙色分配给所有像素。 要将颜色分配给像素,可以在片段着色器的main()
函数内部使用gl_FragColor
内置变量。
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0.5, 0, 1.0);
}
在上面的代码中,第一行指定浮点数的精度很重要,因为片段着色器对其没有任何默认精度。
步骤3:编译着色器
回到Torus
类中,现在必须添加代码来编译创建的两个着色器。 但是,在执行此操作之前,必须将它们从原始资源转换为字符串。 IOUtils
类是Apache Commons IO库的一部分,具有用于执行此操作的toString()
方法。 以下代码显示了如何使用它:
// Convert vertex_shader.txt to a string
InputStream vertexShaderStream =
context.getResources().openRawResource(R.raw.vertex_shader);
String vertexShaderCode =
IOUtils.toString(vertexShaderStream, Charset.defaultCharset());
vertexShaderStream.close();
// Convert fragment_shader.txt to a string
InputStream fragmentShaderStream =
context.getResources().openRawResource(R.raw.fragment_shader);
String fragmentShaderCode =
IOUtils.toString(fragmentShaderStream, Charset.defaultCharset());
fragmentShaderStream.close();
着色器的代码必须添加到OpenGL ES着色器对象中。 若要创建一个新的着色器对象,请使用GLES20
类的glCreateShader()
方法。 根据要创建的着色器对象的类型,可以将GL_VERTEX_SHADER
或GL_FRAGMENT_SHADER
传递给它。 该方法返回一个整数,该整数用作对着色器对象的引用。 新创建的着色器对象不包含任何代码。 要将着色器代码添加到着色器对象,必须使用glShaderSource()
方法。
以下代码为顶点着色器和片段着色器创建着色器对象:
int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(vertexShader, vertexShaderCode);
int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fragmentShader, fragmentShaderCode);
现在,我们可以将着色器对象传递给glCompileShader()
方法,以编译它们包含的代码。
GLES20.glCompileShader(vertexShader);
GLES20.glCompileShader(fragmentShader);
8.创建一个程序
渲染3D对象时,不要直接使用着色器。 而是将它们附加到程序上并使用该程序。 因此,将一个成员变量添加到Torus
类中以存储对OpenGL ES程序的引用。
private int program;
若要创建一个新程序,请使用glCreateProgram()
方法。 要将顶点和片段着色器对象附加到该对象,请使用glAttachShader()
方法。
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
此时,您可以链接程序并开始使用它。 为此,请使用glLinkProgram()
和glUseProgram()
方法。
GLES20.glLinkProgram(program);
GLES20.glUseProgram(program);
9.绘制3D对象
准备好着色器和缓冲区后,我们就可以绘制圆环了。 向Torus
类添加一个名为draw的新方法:
public void draw() {
// Drawing code goes here
}
在先前的步骤中,在顶点着色器内部,我们定义了一个position
变量以从Java代码接收顶点位置数据。 现在是时候向其发送顶点位置数据了。 为此,我们必须首先使用glGetAttribLocation()
方法glGetAttribLocation()
Java代码中的position
变量的句柄。 此外,必须使用glEnableVertexAttribArray()
方法启用该句柄。
因此,在draw()
方法内添加以下代码:
int position = GLES20.glGetAttribLocation(program, "position");
GLES20.glEnableVertexAttribArray(position);
要将position
手柄指向我们的顶点缓冲区,必须使用glVertexAttribPointer()
方法。 除了顶点缓冲区本身之外,该方法还期望每个顶点的坐标数,坐标的类型以及每个顶点的字节偏移。 因为每个顶点有三个坐标,并且每个坐标都是一个float
,所以字节偏移量必须为3 * 4
。
GLES20.glVertexAttribPointer(position,
3, GLES20.GL_FLOAT, false, 3 * 4, verticesBuffer);
我们的顶点着色器也需要一个视图投影矩阵。 尽管不一定总是需要这样的矩阵,但是使用矩阵可以更好地控制3D对象的呈现方式。
视图投影矩阵只是视图矩阵和投影矩阵的乘积。 视图矩阵使您可以指定相机的位置及其注视点。 另一方面,投影矩阵使您不仅可以将OpenGL ES的正方形坐标系映射到Android设备的矩形屏幕,还可以指定查看视锥的近平面和远平面。
要创建矩阵,您可以简单地创建三个大小为16
float
数组:
float[] projectionMatrix = new float[16];
float[] viewMatrix = new float[16];
float[] productMatrix = new float[16];
要初始化投影矩阵,可以使用Matrix
类的frustumM()
方法。 它期望左侧,右侧,底部,顶部,近端和远端剪辑平面的位置。 因为我们的画布已经是正方形了,所以可以将值-1
和1
用于左侧和右侧以及底部和顶部剪辑平面。 对于近裁剪平面和远裁剪平面,请随意尝试不同的值。
Matrix.frustumM(projectionMatrix, 0,
-1, 1,
-1, 1,
2, 9);
要初始化视图矩阵,请使用setLookAtM()
方法。 它期望摄像机的位置和所要对准的点。 您可以再次自由尝试不同的值。
Matrix.setLookAtM(viewMatrix, 0,
0, 3, -4,
0, 0, 0,
0, 1, 0);
最后,要使用multiplyMM()
方法来计算乘积矩阵。
Matrix.multiplyMM(productMatrix, 0,
projectionMatrix, 0,
viewMatrix, 0);
要将乘积矩阵传递给顶点着色器,必须使用glGetUniformLocation()
方法获取其matrix
变量的glGetUniformLocation()
。 拥有句柄后,可以使用glUniformMatrix()
方法将其指向乘积矩阵。
int matrix = GLES20.glGetUniformLocation(program, "matrix");
GLES20.glUniformMatrix4fv(matrix, 1, false, productMatrix, 0);
您必须已经注意到,我们还没有使用过Faces缓冲区。 这意味着我们仍然没有告诉OpenGL ES如何连接顶点以形成三角形,这些三角形将用作3D对象的面。
glDrawElements()
方法允许您使用faces缓冲区创建三角形。 作为其参数,它期望顶点索引的总数,每个索引的类型以及面缓冲区。
GLES20.glDrawElements(GLES20.GL_TRIANGLES,
facesList.size() * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);
最后,请记住要禁用先前启用的attribute
处理程序,以将顶点数据传递到顶点着色器。
GLES20.glDisableVertexAttribArray(position);
10.创建一个渲染器
我们的GLSurfaceView
小部件需要一个GLSurfaceView.Renderer
对象才能渲染3D图形。 您可以使用setRenderer()
将渲染器与其关联。
mySurfaceView.setRenderer(new GLSurfaceView.Renderer() {
// More code goes here
});
在渲染器的onSurfaceCreated()
方法内部,必须指定3D图形必须渲染的频率。 现在,让我们仅在3D图形更改时进行渲染。 为此,请将RENDERMODE_WHEN_DIRTY
常量传递给setRenderMode()
方法。 此外,初始化Torus
对象的新实例。
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
mySurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
torus = new Torus(getApplicationContext());
}
在渲染器的onSurfaceChanged()
方法内,您可以使用glViewport()
方法定义视口的宽度和高度。
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0, width, height);
}
在渲染器的onDrawFrame()
方法内部,添加对Torus
类的draw()
方法的调用以实际绘制圆环。
@Override
public void onDrawFrame(GL10 gl10) {
torus.draw();
}
此时,您可以运行您的应用以查看橙色圆环。
结论
您现在知道了如何在Android应用程序中使用OpenGL ES。 在本教程中,您还学习了如何解析Wavefront OBJ文件并从中提取顶点和面数据。 我建议您使用Blender生成更多3D对象,然后尝试在应用程序中渲染它们。
尽管我们只关注OpenGL ES 2.0,但请务必了解OpenGL ES 3.x与OpenGL ES 2.0向后兼容。 这意味着,如果您希望在应用程序中使用OpenGL ES 3.x,则只需将GLES20
类替换为GLES30
或GLES31
类。
翻译自: https://code.tutsplus.com/tutorials/how-to-use-opengl-es-in-android-apps--cms-28464