如何在Android Apps中使用OpenGL ES

当今市场上几乎所有的Android手机都具有图形处理单元,简称GPU。 顾名思义,这是专用于处理通常与3D图形相关的计算的硬件单元。 作为应用程序开发人员,您可以利用GPU来创建以很高的帧速率运行的复杂图形和动画。

当前,您可以使用两种不同的API与Android设备的GPU进行交互: VulkanOpenGL 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图形画布的小部件: GLSurfaceViewTextureView 。 大多数开发人员更喜欢使用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 FacesKeep Vertex Order选项,然后按Export OBJ按钮。

将3D对象导出为Wavefront OBJ文件

现在,您可以关闭Blender并将OBJ文件移动到Android Studio项目的资产文件夹中。

5.解析OBJ文件

如果您尚未注意到,我们在上一步中创建的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_SHADERGL_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()方法。 它期望左侧,右侧,底部,顶部,近端和远端剪辑平面的位置。 因为我们的画布已经是正方形了,所以可以将值-11用于左侧和右侧以及底部和顶部剪辑平面。 对于近裁剪平面和远裁剪平面,请随意尝试不同的值。

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类替换为GLES30GLES31类。

翻译自: https://code.tutsplus.com/tutorials/how-to-use-opengl-es-in-android-apps--cms-28464

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值