前言
都说做事工作要有计划,每天按时完成工作计划剩下的时间都在摸鱼,那可不行,于是指定了本月的形成目标,大家可以一起监督
10.19-24音视频中高级52部+面试
10.25-26高级Android组件化强化实战(一二)
10.27-11.3高级Android组件化强化实战(大厂架构演化20章)
中间所有的周六周日都休息😁
关注公众号:Android苦做舟
解锁 《Android十二大板块文档》
音视频大合集,从初中高到面试应有尽有;让学习更贴近未来实战。已形成PDF版
十二个模块内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书
12.大厂高级Android组件化强化实战
整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔
一丶openGL ES介绍
简介OpenGL ES
谈到OpenGL ES,首先我们应该先去了解一下Android的基本架构,基本架构下图:
在这里我们可以找到Libraries里面有我们目前要接触的库,即OpenGL ES。
根据上图可以知道Android 目前是支持使用开放的图形库的,特别是通过OpenGL ES API来支持高性能的2D和3D图形。OpenGL是一个跨平台的图形API。为3D图形处理硬件指定了一个标准的软件接口。OpenGL ES 是适用于嵌入式设备的OpenGL规范。
基本介绍
Android 能够通过framework框架提供的API或者NDK来支持OpenGL。本文重点介绍框架提供的接口来使用OpenGL的方式,有关于NDK方面的信息,可以自行去官方文档进行了解。
在Android框架里面两个基本的类允许你使用OpenGL ES API创建和操作图形: GLSurfaceView 和 GLSurfaceView.Renderer。如果您的目标是在Android程序中使用OpenGL,那么首先需要做的事情就是了解这两个类。
GLSurfaceView
这是一个视图类,你可以使用OpenGL API来绘制和操作图形对象,这一点在功能上很类似于SurfaceView。你可以通过创建一个SurfaceView的实例并添加你的渲染器来使用这个类。但是如果想要捕捉触摸屏的事件,则应该扩展GLSurfaceView以实现触摸监听器。关于实现触摸监听器的方式,我们会在后面的文章中进行讲解。
GLSurfaceView.Renderer
此接口定义了在GLSurfaceView中绘制图形所需的方法。您必须将此接口的实现作为单独的类提供,并使用GLSurfaceView.setRenderer()
将其附加到您的GLSurfaceView实例。 GLSurfaceView.Renderer要求实现以下方法:
onSurfaceCreated()
:创建GLSurfaceView时,系统调用一次该方法。使用此方法执行只需要执行一次的操作,例如设置OpenGL环境参数或初始化OpenGL图形对象。onDrawFrame()
:系统在每次重画GLSurfaceView时调用这个方法。使用此方法作为绘制(和重新绘制)图形对象的主要执行方法。onSurfaceChanged()
:当GLSurfaceView的发生变化时,系统调用此方法,这些变化包括GLSurfaceView的大小或设备屏幕方向的变化。例如:设备从纵向变为横向时,系统调用此方法。我们应该使用此方法来响应GLSurfaceView容器的改变。
二丶OpenGL ES 环境搭建
环境搭建目的
为了在Android应用程序中使用OpenGL ES绘制图形,必须要为他们创建一个视图容器。其中最直接或者最常用的方式就是实现一个GLSurfaceView和一个GLSurfaceView.Renderer。GLSurfaceView是用OpenGL绘制图形的视图容器,GLSurfaceView.Renderer控制在该视图内绘制的内容。
下面将讲解如何使用GLSurfaceView 和 GLSurfaceView.Renderer 在一个简单的应用程序的Activity上面做一个最小的实现。
在Manifest中声明OpenGL ES使用
添加以下声明到manifest:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
复制代码
如果你的应用程序需要使用纹理压缩,你还需要声明你的应用程序需要支持哪种压缩格式,以便他们安装在兼容的设备上。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
复制代码
创建一个Activity 用于展示OpenGL ES 图形
使用OpenGL ES的应用程序的Activity和其他应用程的Activity一样,不同的地方在于你设置的Activity的布局。在许多使用OpenGL ES的app中,你可以添加TextView,Button和ListView,还可以添加GLSurfaceView。
下面的代码展示了使用GLSurfaceView做为主视图的基本实现:
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
复制代码
注意:请确保你的Android项目针对的版本是否符合。
创建GLSurfaceView对象
GLSurfaceView是一个特殊的View,通过这个View你可以绘制OpenGL图像。但是View本身没有做太多的事情,主要的绘制是通过设置在View里面的GLSurfaceView.Renderer 来控制的。实际上,创建这个对象的代码是很少的,你能会想尝试跳过extends的操作,只去创建一个没有被修改的GLSurfaceView实例,但是不建议这样去做。因为在某些情况下,你需要扩展这个类来捕获触摸的事件,捕获触摸的事件的方式会在后面的文章里面做介绍。 GLSurfaceView的基本代码很少,为了快速的实现,通常会在使用它的Activity中创建一个内部类来做实现:
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(mRenderer);
}
}
复制代码
你可以通过设置GLSurfaceView.RENDERMODE_WHEN_DIRTY
来让你的GLSurfaceView监听到数据变化的时候再去刷新,即修改GLSurfaceView的渲染模式。这个设置可以防止重绘GLSurfaceView,直到你调用了requestRender()
,这个设置在默写层面上来说,对你的APP是更有好处的。
创建一个Renderer类
实现了GLSurfaceView.Renderer 类才是真正算是开始能够在应用中使用OpenGL ES。这个类控制着与它关联的GLSurfaceView 绘制的内容。在renderer 里面有三个方法能够被Android系统调用,以便知道在GLSurfaceView绘制什么以及如何绘制
onSurfaceCreated()
- 在View的OpenGL环境被创建的时候调用。onDrawFrame()
- 每一次View的重绘都会调用onSurfaceChanged()
- 如果视图的几何形状发生变化(例如,当设备的屏幕方向改变时),则调用此方法。
下面是使用OpenGL ES 渲染器的基本实现,仅仅做的事情就是在GLSurfaceView绘制一个黑色背景。
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
复制代码
总结
上述的内容就是基本的OpenGL ES基本的环境配置,本文的代码仅仅是创建一个简单的Android应用然后使用OpenGL展示一个黑板。虽然没有做其他更加有趣的事情,但是,通过创建这些类,你应该已经拥有了使用OpenGL绘制图形元素的基础了。
如果你熟悉OpenGL的API,现在你应该可以在你的APP里面创建一个OpenGL ES的环境,并开始进行画图了。但是如果需要更多的帮助来使用OpenGL,就请期待下面的文章吧。
三丶OpenGL ES定义形状
在上篇文章,我们能够配置好基本的Android OpenGL 使用的环境。但是如果我们不了解OpenGL ES如何定义图像的一些基本知识就使用OpenGL ES进行绘图还是有点棘手的。所以能够在OpenGL ES的View里面定义要绘制的形状是进行高端绘图操作的第一步。 本文主要做的事情就是为了讲解Android设备屏幕相关的OpenGL ES坐标系统,定义形状,形状面的基础知识,以及定义三角形和正方形。
定义三角形
OpenGL ES允许你使用三维空间坐标系定义绘制的图像,所以你在绘制一个三角形之前必须要先定义它的坐标。在OpenGL中,这样做的典型方法是为坐标定义浮点数的顶点数组。 为了获得最大的效率,可以将这些坐标写入ByteBuffer,并传递到OpenGL ES图形管道进行处理。
public class Triangle {
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
}
}
复制代码
默认情况下,OpenGL ES采用坐标系,[0,0,0](X,Y,Z)指定GLSurfaceView框架的中心,[1,1,0]是框架的右上角,[ - 1,-1,0]是框架的左下角。 有关此坐标系的说明,请参阅OpenGL ES开发人员指南。
请注意,此图形的坐标以逆时针顺序定义。 绘图顺序非常重要,因为它定义了哪一面是您通常想要绘制的图形的正面,以及背面。关于这块相关的更多的内容,可以去查看一下相关的OpenGL ES 文档。
定义正方形
可以看到,在OpenGL里面定义一个三角形很简单。但是如果你想要得到一个更复杂一点的东西呢?比如一个正方形?能够找到很多办法来作到这一点,但是在OpenGL里面绘制这个图形的方式是将两个三角形画在一起。
同样,你应该以逆时针的顺序为这两个代表这个形状的三角形定义顶点,并将这些值放在一个ByteBuffer中。 为避免定义每个三角形共享的两个坐标两次,请使用图纸列表告诉OpenGL ES图形管道如何绘制这些顶点。 这是这个形状的代码:
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (### of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (### of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
复制代码
这个例子让你了解用OpenGL创建更复杂的形状的过程。 一般来说,您使用三角形的集合来绘制对象。下面的文章里面,将讲述如何在屏幕上绘制这些形状。
四丶OpenGL ES绘制形状
在上文中,我们使用OpenGL定义了能够被绘制出来的形状了,现在我们想绘制出来它们。
初始化形状
在你做任何绘制操作之前,你必须要初始化并加载你准备绘制的形状。除非形状的结构(指原始的坐标)在执行过程中发生改变,你都应该在你的Renderer的方法onSurfaceCreated()
中进行内存和效率方面的初始化工作。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// initialize a triangle
mTriangle = new Triangle();
// initialize a square
mSquare = new Square();
}
...
}
复制代码
绘制形状
画一个定义好的形状需要比较多的代码,因为你必须为图形渲染管线提供一大堆信息。特别的,你必须定义以下几个东西:
- Vertex Shader - 用于渲染形状的顶点的OpenGLES 图形代码。
- Fragment Shader - 用于渲染形状的外观(颜色或纹理)的OpenGLES 代码。
- Program - 一个OpenGLES对象,包含了你想要用来绘制一个或多个形状的shader。
你至少需要一个vertexshader来绘制一个形状和一个fragmentshader来为形状上色。这些形状必须被编译然后被添加到一个OpenGLES program中,program之后被用来绘制形状。下面是一个展示如何定义一个可以用来绘制形状的基本shader的例子:
public class Triangle {
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
...
}
复制代码
Shader们包含了OpenGLShading Language (GLSL)代码,必须在使用前编译。要编译这些代码,在你的Renderer类中创建一个工具类方法:
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
复制代码
为了绘制你的形状,你必须编译shader代码,添加它们到一个OpenGLES program 对象然后链接这个program。在renderer对象的构造器中做这些事情,从而只需做一次即可。
注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免多次做这些事。如果在运行时你不知道shader的内容,你应该只创建一次code然后缓存它们以避免多次创建。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
}
复制代码
此时,你已经准备好增加真正的绘制调用了。需要为渲染管线指定很多参数来告诉它你想画什么以及如何画。因为绘制操作因形状而异,让你的形状类包含自己的绘制逻辑是个很好主意。
创建一个draw()
方法负责绘制形状。下面的代码设置位置和颜色值到形状的vertexshader和fragmentshader,然后执行绘制功能:
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
复制代码
一旦完成了所有这些代码,绘制该对象只需要在渲染器的onDrawFrame()
方法中调用draw()
方法:
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
复制代码
当你运行程序的时候,你就应该看到以下的内容: When you run the application, it should look something like this:
此例子中的代码还有很多问题。首先,它不会打动你和你的朋友。其次,三角形会在你从竖屏变为横屏时被压扁。三角形变形的原因是其顶点们没有跟据屏幕的宽高比进行修正。而且这里展示出来的三角形是静止的,这样的图形是有点无聊的,在“添加动画”的文章中,我们会使用OpenGL ES 的视图管线来旋转此形状。
五丶OpenGL ES使用投影和相机视图
OpenGL ES环境允许你以更接近于你眼睛看到的物理对象的方式来显示你绘制的对象。物理查看的模拟是通过对你所绘制的对象的坐标进行数学变换完成的:
-
Projection — 这个变换是基于他们所显示的GLSurfaceView的宽和高来调整绘制对象的坐标的。没有这个计算变换,通过OpenGL绘制的形状会在不同显示窗口变形。这个投影变化通常只会在OpenGL view的比例被确定或者在你渲染器的
onSurfaceChanged()
方法中被计算。想要了解更多的关于投影和坐标映射的相关信息,请看绘制对象的坐标映射。 -
Camera View — 这个换是基于虚拟的相机的位置来调整绘制对象坐标的。需要着重注意的是,OpenGL ES并没有定义一个真实的相机对象,而是提供一个实用方法,通过变换绘制对象的显示来模拟一个相机。相机视图变换可能只会在你的GLSurfaceView被确定时被计算,或者基于用户操作或你应用程序的功能来动态改变。
本课程描述怎样创建投影和相机视图并将其应用的到你的GLSurfaceView的绘制对象上。
定义投影
投影变化的数据是在你GLSurfaceView.Renderer类的onSurfaceChanged()
方法中被计算的。下面的示例代码是获取GLSurfaceView的高和宽,并通过Matrix.frustumM()
方法用它们填充到投影变换矩阵中。
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
复制代码
上面的代码填充有一个投影矩阵mProjectionMatrix,mProjectionMatrix可以在onFrameDraw()方法中与下一部分的相机视图结合在一起。
注意:如果仅仅只把投影矩阵应用的到你绘制的对象中,通常你只会得到一个非常空的显示。一般情况下,你还必须为你要在屏幕上显示的任何内容应用相机视图。
定义相机视图
通过在你的渲染器中添加相机视图变换作为你绘制过程的一部分来完成你的绘制图像的变换过程。在下面的代码中,通过Matrix.setLookAtM()
方法计算相机视图变换,然后将其与之前计算出的投影矩阵结合到一起。合并后的矩阵接下来会传递给绘制的图形。
@Override
public void onDrawFrame(GL10 unused) {
...
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Draw shape
mTriangle.draw(mMVPMatrix);
}
复制代码
应用投影和相机变换
为了使用在上一部分内容中展示的投影和相机视图变换的合并矩阵,首先要在之前Triangle类中定义的定点着色器代码中添加一个矩阵变量:
public class Triangle {
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// Use to access and set the view transformation
private int mMVPMatrixHandle;
...
}
复制代码
下一步,修改你的图形对象的draw()
方法来接收联合变换矩阵,并将它们应用到图形中:
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
...
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
复制代码
一旦你正确的计算并应用投影和相机视图变换,你的绘图对象将会以正确的比例绘制,它看起来应该像下面这样:
现在你已经有一个可以以正确比例显示图形的应用了。后面的章节,我们可以了解如何为你的图形添加运动了。
六丶OpenGL ES添加运动效果
在屏幕上绘制图形只是OpenGL的相当基础的特点,你也可以用其他的Android图形框架类来实现这些,包括Canvas和Drawable对象。OpenGL ES为在三维空间中移动和变换提供了额外的功能,并提供了创建引人注目的用户体验的独特方式。 在本文中,你将进一步使用OpenGL ES学习怎样为你的图形添加一个旋转动作。
旋转一个图形
用来旋转一个绘制对象是相对简单的。在你的渲染器中,添加一个新的变换矩阵(旋转矩阵),然后把它与你的投影与相机视图变换矩阵合并到一起:
private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// Create a rotation transformation for the triangle
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
复制代码
如果做了这些改变后你的三角形还没有旋转,请确保你是否注释掉了GLSurfaceView.RENDERMODE_WHEN_DIRTY
设置项,这将在下一部分讲到。
允许连续渲染
如果你勤恳地遵循本系列课程的示例代码到这个点,请确保你注释了设置只有当dirty的时候才渲染的渲染模式这一行,否则OpenGL旋转图形,只会递增角度然后等待来自GLSurfaceView容器的对requestRender()
方法的调用:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data.
// To allow the triangle to rotate automatically, this line is commented out:
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
复制代码
除非你的对象改变没有用户交互,否则通常打开这个标志是个好主意。准备好取消注释这行代码,因为下一节内容将使这个调用再次适用。
七丶OpenGL ES 响应触摸事件
像旋转三角形一样,通过预设程序来让对象移动对于吸引注意是很有用的,但是如果你想让你的OpenGL图形有用户交互呢?让你的OpenGL ES应用有触摸交互的关键是,扩展你的GLSurfaceView的实现重载onTouchEvent()
方法来监听触摸事件。 本节内容将向你展示如何监听触摸事件来让用户旋转一个图形。
设置触摸事件
为了你的OpenGL ES应用能够响应触摸事件,你必须在你的GLSurfaceView中实现onTouchEvent()
方法,下面的实现例子展示了怎样监听MotionEvent.ACTION_MOVE
事件,并将该事件转换成图形的旋转角度。
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(
mRenderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
复制代码
需要注意的是,计算完旋转角度后,需要调用requestRender()
方法来告诉渲染器是时候渲染帧画面了。在本例子中这种方法是最高效的,因为除非旋转有改变,否则帧画面不需要重绘。然而除非你还用setRenderMode()
方法要求渲染器只有在数据改变时才进行重绘,否则这对性能没有任何影响。因此,确保渲染器中的下面这行是取消注释的:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
复制代码
暴露旋转角度
上面的例程代码中需要你通过在渲染器中添加共有的成员来暴露旋转角度。当渲染代码是在独立于你应用程序的主用户界面线程的单独线程执行的时候,你必须声明这个共有变量是volatile类型的。下面的代码声明了这个变量并且暴露了它的getter和setter方法对:
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
复制代码
应用旋转
为了应用触摸输入产生的旋转,先注释掉产生角度的代码,并添加一个右触摸事件产生的角度mAngle:
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
复制代码
当你完成上面介绍的步骤,运行你的程序,然后在屏幕上拖拽你的手指来旋转这个三角形。
八丶OpenGL ES 着色器语言GLSL
前面的文章主要是整理的Android 官方文档对OpenGL ES支持的介绍。通过之前的文章,我们基本上可以完成的基本的形状的绘制。
目前到这里第一阶段的学习,也就是基本的图形绘制,基本的交互的实现。
- 平面绘制:三角形、正方形、在相机视角下的三角形、彩色三角形
- 立体绘制:正方体、圆柱体、圆锥体、球体
- 基本交互:手绘点、旋转三角形
知道了基本的图形绘制,也知道了基本的交互的实现,现在可能大多数人还是对整个实现的流程有点懵,最主要的地方可能就是对顶点着色器和片元着色器了。前面的使用过程中,我们大概也对着色器语言有一定的了解了,但是在前面我们使用的着色器代码还是很简单的,做的事情也是很有限的,后面的开发过程中,我们用到的着色器会越来越复杂,So,这里我们想一下着色器语言GLSL。
我们知道,在OpenGL ES中着色器分为顶点着色器和片元着色器。顶点着色器是针对每个顶点执行一次,用于确定顶点的位置。片元着色器是针对每个片元,片元我们可以理解为每个像素,用于确定每个片元(像素)的颜色。
GLSL 简介
GLSL又叫OpenGL着色语言(OpenGL Shading Language),是用来在OpenGL中着色编程的语言,是一种面向过程的语言,基本的语法和C/C++基本相同,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器)。
在前面的学习中,我们基本上使用的都是非常简单的着色器,基本上没有使用过GLSL的内置函数,但是在后面我们完成其他的功能的时候应该就会用到这些内置函数了。
GLSL 基础
GLSL 虽然很类似于C/C++,但是它和C/C++还是有很大的不同的,比如,没有double,long等类型,没有union、enum、unsigned以及位运算等特性。
基本数据类型
GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:
标量:
标量表示的是只有大小没有方向的量,在GLSL中标量只有bool、int和float三种。对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。对于标量的运算,我们最需要注意的是精度,防止溢出问题。
向量:
向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四位向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)。向量在GPU中由硬件支持运算,比CPU快的多。
- 作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。
- 作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。
- 作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。
矩阵:
在GLSL中矩阵拥有22、33、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。
采样器:
采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。
结构体:
和C语言中的结构体相同,用struct来定义结构体,关于结构体参考C语言中的结构体。
数组:
数组知识也和C中相同,不同的是数组声明时可以不指定大小,但是建议在不必要的情况下,还是指定大小的好。
空类型:
空类型用void表示,仅用来声明不返回任何值得函数。
数据声明示例:
float a=1.0;
int b=1;
bool c=true;
vec2 d=vec2(1.0,2.0);
vec3 e=vec3(1.0,2.0,3.0)
vec4 f=vec4(vec3,1.2);
vec4 g=vec4(0.2); //相当于vec(0.2,0.2,0.2,0.2)
vec4 h=vec4(a,a,1.3,a);
mat2 i=mat2(0.1,0.5,1.2,2.4);
mat2 j=mat2(0.8); //相当于mat2(0.8,0.8,0.8,0.8)
mat3 k=mat3(e,e,1.2,1.6,1.8);
复制代码
运算符
GLSL中的运算符有(越靠前,运算优先级越高):
- 索引:[]
- 前缀自加和自减:++,–
- 一元非和逻辑非:~,!
- 加法和减法:+,-
- 等于和不等于:==,!=
- 逻辑异或:^^
- 三元运算符号,选择:?:
- 成员选择与混合:.
- 后缀自加和自减:++,–
- 乘法和除法:*,/
- 关系运算符:>,<,=,>=,<=,<>
- 逻辑与:&&
- 逻辑或:||
- 赋值预算:=,+=,-=,*=,/=
类型转换
GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1;也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float。
限定符
在之前的博客中也提到了,GLSL中的限定符号主要有:
- attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。
- uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。
- varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。
- const:常量。 限定符与java限定符类似,放在变量类型之前,并且只能用于全局变量。在GLSL中,没有默认限定符一说。
流程控制
GLSL中的流程控制与C中基本相同,主要有:
- if(){}、if(){}else{}、if(){}else if(){}else{}
- while(){}和do{}while()
- for(;😉{}
- break和continue
函数
GLSL中也可以定义函数,定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下:
- in:输入参数,无修饰符时默认为此修饰符。
- out:输出参数。
- inout:既可以作为输入参数,又可以作为输出参数。
浮点精度
与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度,否则编译会报错。精度有三种,分别为:
- lowp:低精度。8位。
- mediump:中精度。10位。
- highp:高精度。16位。
不仅仅是float可以制定精度,其他(除了bool相关)类型也同样可以,但是int、采样器类型并不一定要求指定精度。加精度的定义如下:
uniform lowp float a=1.0;
varying mediump vec4 c;
复制代码
当然,也可以在片元着色器中设置默认精度,只需要在片元着色器最上面加上precision <精度> <类型>即可制定某种类型的默认精度。其他情况相同的话,精度越高,画质越好,使用的资源也越多。
程序结构
前面几篇博客都有使用到着色器,我们对着色器的程序结构也应该有一定的了解。也许一直沉浸在Android应用开发,没有了解C开发的朋友,对这种结构并不熟悉。GLSL程序的结构和C语言差不多,main()方法表示入口函数,可以在其上定义函数和变量,在main中可以引用这些变量和函数。定义在函数体以外的叫做全局变量,定义在函数体内的叫做局部变量。与高级语言不通的是,变量和函数在使用前必须声明,不能再使用的后面声明变量或者函数。
GLSL 内建变量
在着色器中我们一般都会声明变量来在程序中使用,但是着色器中还有一些特殊的变量,不声明也可以使用。这些变量叫做内建变量。內建变量,相当于着色器硬件的输入和输出点,使用者利用这些输入点输入之后,就会看到屏幕上的输出。通过输出点可以知道输出的某些数据内容。当然,实际上肯定不会这样简单,这么说只是为了帮助理解。在顶点着色器中的内建变量和片元着色器的内建变量是不相同的。着色器中的内建变量有很多,在此,我们只列出最常用的集中内建变量。
顶点着色器的内建变量
输入变量:
- gl_Position:顶点坐标
- gl_PointSize:点的大小,没有赋值则为默认值1,通常设置绘图为点绘制才有意义。\
片元着色器的内建变量
输入变量:
- gl_FragCoord:当前片元相对窗口位置所处的坐标。
- gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。 输出变量:
- gl_FragColor:当前片元颜色
- gl_FragData:vec4类型的数组。向其写入的信息,供渲染管线的后继过程使用。
常用内置函数
常见函数
- radians(x):角度转弧度
- degrees(x):弧度转角度
- sin(x):正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦、atan反正切
- pow(x,y):xy
- exp(x):ex
- exp2(x):2x
- log(x):logex
- log2(x):log2x
- sqrt(x):x√
- inversesqr(x):1x√
- abs(x):取x的绝对值
- sign(x):x>0返回1.0,x<0返回-1.0,否则返回0.0
- ceil(x):返回大于或者等于x的整数
- floor(x):返回小于或者等于x的整数
- fract(x):返回x-floor(x)的值
- mod(x,y):取模(求余)
- min(x,y):获取xy中小的那个
- max(x,y):获取xy中大的那个
- mix(x,y,a):返回x∗(1−a)+y∗a
- step(x,a):x< a返回0.0,否则返回1.0
- smoothstep(x,y,a):a < x返回0.0,a>y返回1.0,否则返回0.0-1.0之间平滑的Hermite插值。
- dFdx(p):p在x方向上的偏导数
- dFdy(p):p在y方向上的偏导数
- fwidth(p):p在x和y方向上的偏导数的绝对值之和
几何函数
- length(x):计算向量x的长度
- distance(x,y):返回向量xy之间的距离
- dot(x,y):返回向量xy的点积
- cross(x,y):返回向量xy的差积
- normalize(x):返回与x向量方向相同,长度为1的向量
矩阵函数
- matrixCompMult(x,y):将矩阵相乘
- lessThan(x,y):返回向量xy的各个分量执行x< y的结果,类似的有greaterThan,equal,notEqual
- lessThanEqual(x,y):返回向量xy的各个分量执行x<= y的结果,类似的有类似的有greaterThanEqual
- any(bvec x):x有一个元素为true,则为true
- all(bvec x):x所有元素为true,则返回true,否则返回false
- not(bvec x):x所有分量执行逻辑非运算
纹理采样函数
纹理采样函数有texture2D、texture2DProj、texture2DLod、texture2DProjLod、textureCube、textureCubeLod及texture3D、texture3DProj、texture3DLod、texture3DProjLod等。
- texture表示纹理采样,2D表示对2D纹理采样,3D表示对3D纹理采样
- Lod后缀,只适用于顶点着色器采样
- Proj表示纹理坐标st会除以q
纹理采样函数中,3D在OpenGLES2.0并不是绝对支持。我们再次暂时不管3D纹理采样函数。重点只对texture2D函数进行说明。texture2D拥有三个参数,第一个参数表示纹理采样器。第二个参数表示纹理坐标,可以是二维、三维、或者四维。第三个参数加入后只能在片元着色器中调用,且只对采样器为mipmap类型纹理时有效。
九丶OpenGL ES纹理贴图
概念
一般说来,纹理是表示物体表面的一幅或几幅二维图形,也称纹理贴图(texture)。当把纹理按照特定的方式映射到物体表面上的时候,能使物体看上去更加真实。当前流行的图形系统中,纹理绘制已经成为一种必不可少的渲染方法。在理解纹理映射时,可以将纹理看做应用在物体表面的像素颜色。在真实世界中,纹理表示一个对象的颜色、图案以及触觉特征。纹理只表示对象表面的彩色图案,它不能改变对象的几何形式。更进一步的说,它只是一种高强度的计算行为。 概括为一句就是:纹理贴图就是把一个纹理(对于2D贴图,可以简单的理解为图片),按照所期望的方式显示在诸多三角形组成的物体的表面。
原理
首先介绍一下纹理映射时的坐标系,纹理映射的坐标系和顶点着色器的坐标系是不一样的。 纹理坐标用浮点数来表示,范围一般从0.0到1.0,左上角坐标为(0.0,0.0),右上角坐标为(1.0,0.0),左下角坐标为(0.0,1.0),右下角坐标为(1.0,1.0),具体如下:
顶点着色器的坐标系如下:
将纹理映射到右边的两个三角形上(也就是一个矩形),需要将纹理坐标指定到正确的顶点上,才能使纹理正确的显示,否则显示出来的纹理会无法显示,或者出现旋转、翻转、错位等情况。 将右图顶点按照V2V1V4V3传入,以三角形条带方式绘制,则纹理坐标应按照V2V1V4V3传入。如果按照V3V4V1V2传入,会得到一个旋转了180度的纹理。如果按照V4V3V2V1传入,则会得到一个左右翻转的纹理。
显示纹理图片
我们可以根据以下步骤利用OpenGL ES显示一张图片:
修改着色器
首先,我们需要修改我们的着色器,将顶点着色器修改为:
attribute vec4 vPosition;
attribute vec2 vCoordinate;
uniform mat4 vMatrix;
varying vec2 aCoordinate;
void main(){
gl_Position=vMatrix*vPosition;
aCoordinate=vCoordinate;
}
复制代码
可以看到,顶点着色器中增加了一个vec2变量,并将这个变量传递给了片元着色器,这个变量就是纹理坐标。接着我们修改片元着色器为:
precision mediump float;
uniform sampler2D vTexture;
varying vec2 aCoordinate;
void main(){
gl_FragColor=texture2D(vTexture,aCoordinate);
}
复制代码
片元着色器中,增加了一个sampler2D的变量,sampler2D我们在前一篇博客GLSL语言基础中提到过,是GLSL的变量类型之一的取样器。texture2D也有提到,它是GLSL的内置函数,用于2D纹理取样,根据纹理取样器和纹理坐标,可以得到当前纹理取样得到的像素颜色。
设置顶点坐标和纹理坐标
根据纹理映射原理中的介绍,我们将顶点坐标设置为:
private final float[] sPos={
-1.0f,1.0f, //左上角
-1.0f,-1.0f, //左下角
1.0f,1.0f, //右上角
1.0f,-1.0f //右下角
};
复制代码
相应的,对照顶点坐标,我们可以设置纹理坐标为:
private final float[] sCoord={
0.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
1.0f,1.0f,
};
复制代码
计算变换矩阵
按照上步设置顶点坐标和纹理坐标,大多数情况下我们得到的一定是一张拉升或者压缩的图片。为了让图片完整的显示,且不被拉伸和压缩,我们需要向绘制等腰直角三角形一样,计算一个合适的变换矩阵,传入顶点着色器,代码如下:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width,height);
int w=mBitmap.getWidth();
int h=mBitmap.getHeight();
float sWH=w/(float)h;
float sWidthHeight=width/(float)height;
if(width>height){
if(sWH>sWidthHeight){
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight*sWH,sWidthHeight*sWH, -1,1, 3, 7);
}else{
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight/sWH,sWidthHeight/sWH, -1,1, 3, 7);
}
}else{
if(sWH>sWidthHeight){
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1/sWidthHeight*sWH, 1/sWidthHeight*sWH,3, 7);
}else{
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH/sWidthHeight, sWH/sWidthHeight,3, 7);
}
}
//设置相机位置
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
//计算变换矩阵
Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0);
}
复制代码
mMVPMatrix即为我们所需要的变换矩阵。
显示图片
然后我们需要做的,就和之前绘制正方形一样容易了。和之前不同的是,在绘制之前,我们还需要将纹理和纹理坐标传入着色器:
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT);
GLES20.glUseProgram(mProgram);
onDrawSet();
GLES20.glUniformMatrix4fv(glHMatrix,1,false,mMVPMatrix,0);
GLES20.glEnableVertexAttribArray(glHPosition);
GLES20.glEnableVertexAttribArray(glHCoordinate);
GLES20.glUniform1i(glHTexture, 0);
textureId=createTexture();
//传入顶点坐标
GLES20.glVertexAttribPointer(glHPosition,2,GLES20.GL_FLOAT,false,0,bPos);
//传入纹理坐标
GLES20.glVertexAttribPointer(glHCoordinate,2,GLES20.GL_FLOAT,false,0,bCoord);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
}
public abstract void onDrawSet();
public abstract void onDrawCreatedSet(int mProgram);
private int createTexture(){
int[] texture=new int[1];
if(mBitmap!=null&&!mBitmap.isRecycled()){
//生成纹理
GLES20.glGenTextures(1,texture,0);
//生成纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]);
//设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
//设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
//设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
//设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
//根据以上指定的参数,生成一个2D纹理
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
return texture[0];
}
return 0;
}
复制代码
这样我们就可以显示出我们需要显示的图片,并且保证它完整的居中显示而且不会变形了。
十丶通过GLES20与着色器交互
获取着色器程序内成员变量的id(句柄、指针)
GLES20.glGetAttribLocation方法:获取着色器程序中,指定为attribute类型变量的id。 GLES20.glGetUniformLocation方法:获取着色器程序中,指定为uniform类型变量的id。
如:
// 获取指向着色器中aPosition的index
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
// 获取指向着色器中uMVPMatrix的index
muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
复制代码
向着色器传递数据
使用上一节获取的指向着色器相应数据成员的各个id,就能将我们自己定义的顶点数据、颜色数据等等各种数据传递到着色器当中了。
// 使用shader程序
GLES20.glUseProgram(mProgram);
// 将最终变换矩阵传入shader程序
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
// 设置缓冲区起始位置
mRectBuffer.position(0);
// 顶点位置数据传入着色器
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer);
// 顶点颜色数据传入着色器中
GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer);
// 顶点坐标传递到顶点着色器
GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer);
// 允许使用顶点坐标数组
GLES20.glEnableVertexAttribArray(maPositionHandle);
// 允许使用顶点颜色数组
GLES20.glDisableVertexAttribArray(maColorHandle);
// 允许使用定点纹理数组
GLES20.glEnableVertexAttribArray(maTextureHandle);
// 绑定纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
// 图形绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
复制代码
定义顶点属性数组
void glVertexAttribPointer (int index, int size, int type, boolean normalized, int stride, Buffer ptr )
复制代码
参数含义: index 指定要修改的顶点着色器中顶点变量id; size 指定每个顶点属性的组件数量。必须为1、2、3或者4。如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a)); type 指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT; normalized 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE); stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。如果normalized被设置为GL_TRUE,意味着整数型的值会被映射至区间[-1,1],或者区间[0,1](无符号整数),反之,这些值会被直接转换为浮点值而不进行归一化处理; ptr 顶点的缓冲数据。
启用或者禁用顶点属性数组
调用GLES20.glEnableVertexAttribArray和GLES20.glDisableVertexAttribArray传入参数index。
GLES20.glEnableVertexAttribArray(glHPosition);
GLES20.glEnableVertexAttribArray(glHCoordinate);
复制代码
如果启用,那么当GLES20.glDrawArrays或者GLES20.glDrawElements被调用时,顶点属性数组会被使用。
选择活动纹理单元。
void glActiveTexture (int texture)
复制代码
texture指定哪一个纹理单元被置为活动状态。texture必须是GL_TEXTUREi之一,其中0 <= i < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,初始值为GL_TEXTURE0。 GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理,纹理单元的数量是依据该纹理单元所被支持的具体实现。
十一丶OpenSL ES
1.Android OpenSL 介绍和开发流程说明
Android OpenSL ES 介绍
OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化, 高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,降低执行难度,促进高级音频市场的发展。简单来说OpenSL ES是一个嵌入式跨平台免费的音频处理库。
Android的OpenSL ES库是在NDK的platforms文件夹对应android平台先相应cpu类型里面,如:
Android OpenSL ES 开发流程
OpenSL ES 的开发流程主要有如下6个步骤:
1、 创建接口对象
2、设置混音器
3、创建播放器(录音器)
4、设置缓冲队列和回调函数
5、设置播放状态
6、启动回调函数
注明:其中第4步和第6步是OpenSL ES 播放PCM等数据格式的音频是需要用到的。
在使用OpenSL ES的API之前,需要引入OpenSL ES的头文件,代码如下:
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
复制代码
由于是在Native层使用该特性,所需需要在Android.mk中增加链接选项,以便在链接阶段使用到系统系统的OpenSL ES的so库:
LOCAL_LDLIBS += -lOepnSLES
复制代码
我们知道OpenSL ES提供的是基于C语言的API,但是它是基于对象和接口的方式提供的,会采用面向对象的思想开发API。因此我们先来了解一下OpenSL ES中对象和接口的概念:
- 对象:对象是对一组资源及其状态的抽象,每个对象都有一个在其创建时指定的类型,类型决定了对象可以执行的任务集,对象有点类似于C++中类的概念。
- 接口:接口是对象提供的一组特征的抽象,这些抽象会为开发者提供一组方法以及每个接口的类型功能,在代码中,接口的类型由接口ID来标识。
需要重点理解的是,一个对象在代码中其实是没有实际的表示形式的,可以通过接口来改变对象的状态以及使用对象提供的功能。对象有可以有一个或者多个接口的实例,但是接口实例肯定只属于一个对象。
如果明白了OpenSL ES 中对象和接口的概念,那么下面我们就继续看看,在代码中是如何使用它们的。
上面我们也提到过,对象是没有实际的代码表示形式的,对象的创建也是通过接口来完成的。通过获取对象的方法来获取出对象,进而可以访问对象的其他的接口方法或者改变对象的状态,下面是使用对象和接口的相关说明。
OpenSL ES 开发最重要的接口类 SLObjectItf
通过SLObjectItf接口类我们可以创建所需要的各种类型的类接口,比如:
- 创建引擎接口对象:SLObjectItf engineObject
- 创建混音器接口对象:SLObjectItf outputMixObject
- 创建播放器接口对象:SLObjectItf playerObject
以上等等都是通过SLObjectItf来创建的。
SLObjectItf 创建的具体的接口对象实例
OpenSL ES中也有具体的接口类,比如(引擎:SLEngineItf,播放器:SLPlayItf,声音控制器:SLVolumeItf等等)。
创建引擎并实现
OpenSL ES中开始的第一步都是声明SLObjectItf接口类型的引擎接口对象engineObject,然后用方法slCreateEngine创建一个引擎接口对象;创建好引擎接口对象后,需要用SLObjectItf的Realize方法来实现engineObject;最后用SLObjectItf的GetInterface方法来初始化SLEngnineItf对象实例。如:
SLObjectItf engineObject = NULL;//用SLObjectItf声明引擎接口对象
SLEngineItf engineEngine = NULL;//声明具体的引擎对象实例
void createEngine()
{
SLresult result;//返回结果
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);//第一步创建引擎
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);//实现(Realize)engineObject接口对象
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);//通过engineObject的GetInterface方法初始化engineEngine
}
复制代码
利用引擎对象创建其他接口对象
其他接口对象(SLObjectItf outputMixObject,SLObjectItf playerObject)等都是用引擎接口对象创建的(具体的接口对象需要的参数这里就说了,可参照ndk例子里面的),如:
//混音器
SLObjectItf outputMixObject = NULL;//用SLObjectItf创建混音器接口对象
SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;////创建具体的混音器对象实例
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);//利用引擎接口对象创建混音器接口对象
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);//实现(Realize)混音器接口对象
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);//利用混音器接口对象初始化具体混音器实例
//播放器
SLObjectItf playerObject = NULL;//用SLObjectItf创建播放器接口对象
SLPlayItf playerPlay = NULL;//创建具体的播放器对象实例
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &audioSrc, &audioSnk, 3, ids, req);//利用引擎接口对象创建播放器接口对象
result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);//实现(Realize)播放器接口对象
result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);//初始化具体的播放器对象实例
复制代码
最后就是使用创建好的具体对象实例来实现具体的功能。
2.OpenSL ES 使用示例
首先导入OpenSL ES和其他必须的库:
-lOpenSLES -landroid
复制代码
播放assets文件
创建引擎——>创建混音器——>创建播放器——>设置播放状态
JNIEXPORT void JNICALL
Java_com_renhui_openslaudio_MainActivity_playAudioByOpenSL_1assets(JNIEnv *env, jobject instance, jobject assetManager, jstring filename) {
release();
const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL);
// use asset manager to open asset by filename
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN);
(*env)->ReleaseStringUTFChars(env, filename, utf8);
// open asset as file descriptor
off_t start, length;
int fd = AAsset_openFileDescriptor(asset, &start, &length);
AAsset_close(asset);
SLresult result;
//第一步,创建引擎
createEngine();
//第二步,创建混音器
const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
(void)result;
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
(void)result;
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);
if (SL_RESULT_SUCCESS == result) {
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb, &reverbSettings);
(void)result;
}
//第三步,设置播放器参数和创建播放器
// 1、 配置 audio source
SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length};
SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED};
SLDataSource audioSrc = {&loc_fd, &format_mime};
// 2、 配置 audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&loc_outmix, NULL};
// 创建播放器
const SLInterfaceID ids[3] = {SL_IID_SEEK, SL_IID_MUTESOLO, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &fdPlayerObject, &audioSrc, &audioSnk, 3, ids, req);
(void)result;
// 实现播放器
result = (*fdPlayerObject)->Realize(fdPlayerObject, SL_BOOLEAN_FALSE);
(void)result;
// 得到播放器接口
result = (*fdPlayerObject)->GetInterface(fdPlayerObject, SL_IID_PLAY, &fdPlayerPlay);
(void)result;
// 得到声音控制接口
result = (*fdPlayerObject)->GetInterface(fdPlayerObject, SL_IID_VOLUME, &fdPlayerVolume);
(void)result;
//第四步,设置播放状态
if (NULL != fdPlayerPlay) {
result = (*fdPlayerPlay)->SetPlayState(fdPlayerPlay, SL_PLAYSTATE_PLAYING);
(void)result;
}
//设置播放音量 (100 * -50:静音 )
(*fdPlayerVolume)->SetVolumeLevel(fdPlayerVolume, 20 * -50);
}
复制代码
播放pcm文件
(集成到ffmpeg时,也是播放ffmpeg转换成的pcm格式的数据),这里为了模拟是直接读取的pcm格式的音频文件。
创建播放器和混音器
//第一步,创建引擎
createEngine();
//第二步,创建混音器
const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
(void)result;
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
(void)result;
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);
if (SL_RESULT_SUCCESS == result) {
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
outputMixEnvironmentalReverb, &reverbSettings);
(void)result;
}
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&outputMix, NULL};
复制代码
设置pcm格式的频率位数等信息并创建播放器
// 第三步,配置PCM格式信息
SLDataLocator_AndroidSimpleBufferQueue android_queue={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
SLDataFormat_PCM pcm={
SL_DATAFORMAT_PCM,//播放pcm格式的数据
2,//2个声道(立体声)
SL_SAMPLINGRATE_44_1,//44100hz的频率
SL_PCMSAMPLEFORMAT_FIXED_16,//位数 16位
SL_PCMSAMPLEFORMAT_FIXED_16,//和位数一致就行
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右)
SL_BYTEORDER_LITTLEENDIAN//结束标志
};
SLDataSource slDataSource = {&android_queue, &pcm};
const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slDataSource, &audioSnk, 3, ids, req);
//初始化播放器
(*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);
// 得到接口后调用 获取Player接口
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &pcmPlayerPlay);
复制代码
设置缓冲队列和回调函数
// 注册回调缓冲区 获取缓冲队列接口
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
//缓冲接口回调
(*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, NULL);
复制代码
回调函数:
void * pcmBufferCallBack(SLAndroidBufferQueueItf bf, void * context)
{
//assert(NULL == context);
getPcmData(&buffer);
// for streaming playback, replace this test by logic to find and fill the next buffer
if (NULL != buffer) {
SLresult result;
// enqueue another buffer
result = (*pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2);
// the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
// which for this code example would indicate a programming error
}
}
复制代码
读取pcm格式的文件:
void getPcmData(void **pcm)
{
while(!feof(pcmFile))
{
fread(out_buffer, 44100 * 2 * 2, 1, pcmFile);
if(out_buffer == NULL)
{
LOGI("%s", "read end");
break;
} else{
LOGI("%s", "reading");
}
*pcm = out_buffer;
break;
}
}
复制代码
设置播放状态并手动开始调用回调函数
// 获取播放状态接口
(*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);
// 主动调用回调函数开始工作
pcmBufferCallBack(pcmBufferQueue, NULL);
复制代码
注意:
在回调函数中result = (pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2),最后的“4410022”是buffer的大小,因为我这里是指定了没读取一次就从pcm文件中读取了“441002*2”个字节,所以可以正常播放,如果是利用ffmpeg来获取pcm数据源,那么实际大小要根据每个AVframe的具体大小来定,这样才能正常播放出声音!(44100 * 2 * 2 表示:44100是频率HZ,2是立体声双通道,2是采用的16位采样即2个字节,所以总的字节数就是:44100 * 2 * 2)
3.使用 OpenSL 播放 PCM 数据
OpenSL ES 是基于NDK也就是c语言的底层开发音频的公开API,通过使用它能够做到标准化, 高性能,低响应时间的音频功能实现方法。
这次是使用OpenSL ES来做一个音乐播放器,它能够播放m4a、mp3文件,并能够暂停和调整音量。
播放音乐需要做一些步骤:
创建声音引擎
首先创建声音引擎的对象接口
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
复制代码
然后实现它
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
复制代码
从声音引擎的对象中抓取声音引擎
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
复制代码
创建"输出混音器"
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);
复制代码
实现输出混合音
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
复制代码
创建声音播放器
创建和实现播放器
// realize the player
result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// get the play interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
assert(SL_RESULT_SUCCESS == result);
(void)result;
复制代码
设置播放缓冲
数据格式配置
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_8,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};
复制代码
数据定位器 就是定位要播放声音数据的存放位置,分为4种:内存位置,输入/输出设备位置,缓冲区队列位置,和midi缓冲区队列位置。 数据定位器配置
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
复制代码
得到了缓存队列接口,并注册
// get the buffer queue interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
&bqPlayerBufferQueue);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// register callback on the buffer queue
result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, NULL);
assert(SL_RESULT_SUCCESS == result);
(void)result;
复制代码
获得其他接口用来控制播放
得到声音特效接口
// get the effect send interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_EFFECTSEND,
&bqPlayerEffectSend);
assert(SL_RESULT_SUCCESS == result);
(void)result;
复制代码
得到音量接口
// get the volume interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// set the player's state to playing
result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
assert(SL_RESULT_SUCCESS == result);
(void)result;
复制代码
提供播放数据
打开音乐文件
// convert Java string to UTF-8
const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL);
assert(NULL != utf8);
// use asset manager to open asset by filename
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
assert(NULL != mgr);
AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN);
// release the Java string and UTF-8
(*env)->ReleaseStringUTFChars(env, filename, utf8);
// the asset might not be found
if (NULL == asset) {
return JNI_FALSE;
}
// open asset as file descriptor
off_t start, length;
int fd = AAsset_openFileDescriptor(asset, &start, &length);
assert(0 <= fd);
AAsset_close(asset);
复制代码
设置播放数据
SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length};
SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED};
SLDataSource audioSrc = {&loc_fd, &format_mime};
复制代码
播放音乐
播放音乐只需要通过播放接口改变播放状态就可以了,暂停也是,停止也是,但是暂停必须之前的播放缓存做了才行,否则那暂停就相当于停止了
result = (*fdPlayerPlay)->SetPlayState(fdPlayerPlay, isPlaying ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_PAUSED);
复制代码
调解音量
SLVolumeItf getVolume()
{
if (fdPlayerVolume != NULL)
return fdPlayerVolume;
else
return bqPlayerVolume;
}
void Java_com_renhui_openslaudio_MainActivity_setVolumeAudioPlayer(JNIEnv env, jclass clazz,
jint millibel)
{
SLresult result;
SLVolumeItf volumeItf = getVolume();
if (NULL != volumeItf) {
result = (volumeItf)->SetVolumeLevel(volumeItf, millibel);
assert(SL_RESULT_SUCCESS == result);
(void)result;
}
}
复制代码
释放资源
关闭app时释放占用资源
void Java_com_renhui_openslaudio_MainActivity_shutdown(JNIEnv* env, jclass clazz)
{
// destroy buffer queue audio player object, and invalidate all associated interfaces
if (bqPlayerObject != NULL) {
(*bqPlayerObject)->Destroy(bqPlayerObject);
bqPlayerObject = NULL;
bqPlayerPlay = NULL;
bqPlayerBufferQueue = NULL;
bqPlayerEffectSend = NULL;
bqPlayerMuteSolo = NULL;
bqPlayerVolume = NULL;
}
// destroy file descriptor audio player object, and invalidate all associated interfaces
if (fdPlayerObject != NULL) {
(*fdPlayerObject)->Destroy(fdPlayerObject);
fdPlayerObject = NULL;
fdPlayerPlay = NULL;
fdPlayerSeek = NULL;
fdPlayerMuteSolo = NULL;
fdPlayerVolume = NULL;
}
// destroy output mix object, and invalidate all associated interfaces
if (outputMixObject != NULL) {
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = NULL;
outputMixEnvironmentalReverb = NULL;
}
// destroy engine object, and invalidate all associated interfaces
if (engineObject != NULL) {
(*engineObject)->Destroy(engineObject);
engineObject = NULL;
engineEngine = NULL;
}
}
复制代码
4.OpenSL 录制 PCM 音频数据**
实现说明
OpenSL ES的录音要比播放简单一些,在创建好引擎后,再创建好录音接口基本就可以录音了。在这里我们做的是流式录音,所以需要用至少2个buffer来缓存录制好的PCM数据,这里我们可以动态创建一个二维数组,里面有2个buffer,然后每次录音取出一个,录制好后再写入文件就可以了,2个buffer依次来存储PCM数据,这样就可以连续录制流式音频数据了,二维数组里面自己维护了一个索引,来标识当前处于哪个buffer录制状态,暴露给外部的只是调用方法而已,细节对外也是隐藏的。
编码实现
编写缓存buffer队列:RecordBuffer.h、RecordBuffer.cpp
#ifndef OPENSLRECORD_RECORDBUFFER_H
#define OPENSLRECORD_RECORDBUFFER_H
class RecordBuffer {
public:
short **buffer;
int index = -1;
public:
RecordBuffer(int buffersize);
~RecordBuffer();
/**
* 得到一个新的录制buffer
* @return
*/
short* getRecordBuffer();
/**
* 得到当前录制buffer
* @return
*/
short* getNowBuffer();
};
#endif //OPENSLRECORD_RECORDBUFFER_H
#include "RecordBuffer.h"
RecordBuffer::RecordBuffer(int buffersize) {
buffer = new short *[2];
for(int i = 0; i < 2; i++)
{
buffer[i] = new short[buffersize];
}
}
RecordBuffer::~RecordBuffer() {
}
short *RecordBuffer::getRecordBuffer() {
index++;
if(index > 1)
{
index = 0;
}
return buffer[index];
}
short *RecordBuffer::getNowBuffer() {
return buffer[index];
}
复制代码
这个队列其实就是PCM存储的buffer,getRecordBuffer()为即将要录入PCM数据的buffer,getNowBuffer()是当前录制好的PCM数据的buffer,可以写入文件,即我们得到的PCM数据。
使用OpenSL ES录制PCM数据
过程分为:创建引擎->初始化IO设备(自动检测麦克风等音频输入设备)->设置缓存队列->设置录制PCM数据规格->设置录音器接口->设置队列接口并设置录音状态为录制->开始录音。
const char *path = env->GetStringUTFChars(path_, 0);
/**
* PCM文件
*/
pcmFile = fopen(path, "w");
/**
* PCMbuffer队列
*/
recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2);
SLresult result;
/**
* 创建引擎对象
*/
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
/**
* 设置IO设备(麦克风)
*/
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
SLDataSource audioSrc = {&loc_dev, NULL};
/**
* 设置buffer队列
*/
SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
/**
* 设置录制规格:PCM、2声道、44100HZ、16bit
*/
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN};
SLDataSink audioSnk = {&loc_bq, &format_pcm};
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
/**
* 创建录制器
*/
result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
&audioSnk, 1, id, req);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&recorderBufferQueue);
finished = false;
result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
LOGD("开始录音");
/**
* 开始录音
*/
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
env->ReleaseStringUTFChars(path_, path);
复制代码
录音回调如下:
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
// for streaming recording, here we would call Enqueue to give recorder the next buffer to fill
// but instead, this is a one-time buffer so we stop recording
LOGD("record size is %d", recorderSize);
fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile);
if(finished)
{
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
fclose(pcmFile);
LOGD("停止录音");
} else{
(*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
}
}
复制代码
这样就完成了OPenSL ES的PCM音频数据录制,我们这里拿到了录制的PCM数据可以用mediacodec或ffmpeg来编码成aac格式的音频,也可以直接用推流到服务器来实现音频直播。
完整代码如下:
#include <jni.h>
#include <string>
#include "AndroidLog.h"
#include "RecordBuffer.h"
#include "unistd.h"
extern "C"
{
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
}
//引擎接口
static SLObjectItf engineObject = NULL;
//引擎对象
static SLEngineItf engineEngine;
//录音器接口
static SLObjectItf recorderObject = NULL;
//录音器对象
static SLRecordItf recorderRecord;
//缓冲队列
static SLAndroidSimpleBufferQueueItf recorderBufferQueue;
//录制大小设为4096
#define RECORDER_FRAMES (2048)
static unsigned recorderSize = RECORDER_FRAMES * 2;
//PCM文件
FILE *pcmFile;
//录音buffer
RecordBuffer *recordBuffer;
bool finished = false;
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
// for streaming recording, here we would call Enqueue to give recorder the next buffer to fill
// but instead, this is a one-time buffer so we stop recording
LOGD("record size is %d", recorderSize);
fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile);
if(finished)
{
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
fclose(pcmFile);
LOGD("停止录音");
} else{
(*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_openslrecord_MainActivity_rdSound(JNIEnv *env, jobject instance, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
/**
* PCM文件
*/
pcmFile = fopen(path, "w");
/**
* PCMbuffer队列
*/
recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2);
SLresult result;
/**
* 创建引擎对象
*/
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
/**
* 设置IO设备(麦克风)
*/
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
SLDataSource audioSrc = {&loc_dev, NULL};
/**
* 设置buffer队列
*/
SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
/**
* 设置录制规格:PCM、2声道、44100HZ、16bit
*/
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN};
SLDataSink audioSnk = {&loc_bq, &format_pcm};
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
/**
* 创建录制器
*/
result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
&audioSnk, 1, id, req);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&recorderBufferQueue);
finished = false;
result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
LOGD("开始录音");
/**
* 开始录音
*/
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
env->ReleaseStringUTFChars(path_, path);
}extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_openslrecord_MainActivity_rdStop(JNIEnv *env, jobject instance) {
// TODO
if(recorderRecord != NULL)
{
finished = true;
}
}
复制代码
验证录制成果
有两种方法:
- 使用Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据的demo进行播放。
- 使用 ffplay 命令播放,命令为:ffplay -f s16le -ar 44100 -ac 2 temp.pcm (命令由来:在录制代码里的参数为录制规格:PCM、2声道、44100HZ、16bit)
十二丶OpenSL ES利用SoundTouch实现PCM音频的变速和变调
缘由
OpenSL ES 学习到现在已经知道 OpenSL ES 不仅能播放和录制PCM音频数据,还能改变声音大小、设置左声道或右声道播放、还能变速播放,可谓是播放音频的王者。但是变速有一点不好的就是,虽然播放音频的速度变了,但是相应的音调也随之变了,这样的用户体验就不那么好了。所以就想到了用开源的SoundTouch来实现PCM音频变速和变调,OpenSL ES只是单纯的播放PCM数据就可以了。
实现
移植SoundTouch(Android)
下载SoundTouch源码
在项目jni文件夹中创建include和SoundTouch文件夹,并把下载好的SoundTouch里面的include和SoundTouch的源码拷贝进去就可以了,目录结构如下:
用SoundTouch转码PCM源文件
因为SoundTouch默认是float(32bit)格式的数据,这里需要先改成short(16bit)的格式。打开STTypes.h文件,修改如下代码:
再注释掉下面这句,不然编译不通过(for x86模拟器):
这样SoundTouch里面处理PCM数据就是用的16bit的数据了。
SoundTouch使用流程
添加命名空间,并创建SoundTouch指针变量
using namespace soundtouch;
SoundTouch *soundTouch;
复制代码
设置SoundTouch参数
soundTouch = new SoundTouch();
soundTouch->setSampleRate(44100);//设置采样率,此处为44100,根据实际情况可变
soundTouch->setChannels(2);//声道,此处为立体声
soundTouch->setPitch(1);//变调不变速,如0.5、1.0、1.5等
soundTouch->setTempo(1);//变速不变调,如0.5、1.0、2.0等
复制代码
向SoundTouch中传入获取到的PCM数据,使用:putSamples函数
size = fread(pcm_buffer, 1, 4096 * 2, pcmFile);
soundTouch->putSamples((const SAMPLETYPE *) pcm_buffer, size / 4);
复制代码
这里,pcm_buffer是u_int16_t *类型的,也就是说和SoundTouch处理的PCM数据位数是一致的(16bit),所以可以直接传入SoundTouch中。putSamples的第一个参数就是PCM数据指针,第二个参数是采样点的个数,由于是2声道16bit(2byte),所以PCM数据的采样点个数为:num = 大小(size)/ (2 * 2)。
获取SoundTouch输出的PCM数据:使用receiveSamples函数
num = soundTouch->receiveSamples(sd_buffer, size / 4);
复制代码
这里,receiveSamples的第一个参数是SoundTouch(变速或变调)处理后的PCM数据存放的内存地址,第二个参数是可能的最大采样个数,可以和putSamples保持一致,其中sd_buffer是SAMPLETYPE * 类型的,记得要提前分配好内存大小,最后返回值就是SoundTouch处理后的PCM里面所包含的采样个数,由于可能有缓存,所以应循环读取receiveSamples,直到返回值为0为止。
OpenSL ES播放SoundTouch处理后的PCM音频数据
(*pcmBufferQueue)->Enqueue(pcmBufferQueue, sd_buffer, size * 4);
复制代码
由于size是采样个数,所以sd_buffer的大小是:size * 2(声道) * 2(16bit==2字节)。
这样,我们听到的声音就是通过SoundTouch转码过后的了,如:变速不变调,变调不变速,变速又变调都可以自己设置。
思维发散
*FFmpeg解码得到的PCM数据(uint_8 )利用SoundTouch转码
这里要处理的就是把uint_8 *(8bit)的数据转换成short(16bit)的数据格式。这里其实就是做bit的位运算,原理如下如:
转换代码如下:
for (int i = 0; i < size / 2 + 1; i++)
{
sd_buffer[i] = (pcm_buffer[i * 2] | (pcm_buffer[i * 2 + 1] << 8));
}
soundTouch->putSamples((const SAMPLETYPE *) pcm_buffer, size / 4);
复制代码
后续操作和16bit的一样不变。
总结
虽然是简单的移植SoundTouch到Android来播放PCM数据,但是还是让我们了解到了数据在内存中怎么排列的,然后可以怎么操作最小单位的bit来达到我们的要求。
关注公众号:Android苦做舟
解锁 《Android十二大板块文档》
音视频大合集,从初中高到面试应有尽有;让学习更贴近未来实战。已形成PDF版
十二个模块内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书
12.大厂高级Android组件化强化实战
整理不易,关注一下吧,ღ( ´・ᴗ・` ) 🤔