学习五部曲,弄清楚5个W一个H(when(什么时候使用)、where(在哪个地方使用?)、who(对谁使用)、what(是个什么东西)、why(为什么要这么用?).一个H即:how(到底该怎么用?)),基本的概念篇主要围绕这几个方面进行分析
1. What? openGl是什么?openGl ES又是什么?
相信很多人从事开发的都或多或少听到过有关OpenGl这个东西,但是平时用的少,只知道有这么个东西,而且学起来不简单,所以大多数人都不能讲出个个所以然来。
官方对OpenGl的描述为:
OpenGL(Open Graphics Library开发图形接口)是一个跨平台的图形API,用于指定3D图形处理硬件中的标准软件接口。
OpenGl的前身是SGI公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGl。OpenGl一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGl基本带不动。为此,Khronos公司就为OpenGl提供了一个子集,OpenGl ES(OpenGl for Embedded System)
什么是OpenGl ES呢?
OpenGl ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集。
移动端使用到的基本上都是OpenGl ES,当然Android开发下还专门为OpenGl提供了android.opengl包,并且提供了GlSurfaceView,GLU,GlUtils等工具类。
2. How? Android中的openGL 如何使用?
在了解OpenGl的使用之前,我们需要了解两个基本类别的Android框架:GlSurfaceView和GlSurfaceView.Renderer
3. GlSurfaceView是什么? GLSurfaceView的作用是什么? GLSurfaceView如何使用?
GlSurfaceView从名字就可以看出,它是一个SurfaceView,看源码可知,GlSurfaceView继承自SurfaceView。并增加了Renderer.它的作用就是专门为OpenGl显示渲染使用的。
GLSurfaceView的使用方法:
可以通过创建的实例使用这个类,并增加你的Renderer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
GLSurfaceView glSurfaceView = new GLSurfaceView( this );
glSurfaceView.setRenderer( new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
});
setContentView(glSurfaceView);
}
|
4. GlSurfaceView.Renderer是什么?GLSurfaceView.Renderer的作用?GLSurfaceView.Renderer的用法?
该接口定义了用于绘制在图形所需的方法GLSurfaceView。你必须提供这个接口作为一个单独的类的实现,并将其连接到您的GLSurfaceView使用实例 GLSurfaceView.setRenderer()。如上面的代码所示。作用就是提供各种渲染方法,OpenGl的渲染操作均在此接口中实习。下面说下实现该接口的方法含义:
-
onSurfaceCreated():系统调用这个方法一次创建时GLSurfaceView。使用此方法来执行只需要发生一次的操作,比如设置OpenGL的环境参数或初始化的OpenGL图形对象。
-
onDrawFrame():系统调用上的每个重绘此方法GLSurfaceView。使用此方法作为主要执行点用于绘制(和重新绘制)的图形对象。
-
系统调用此方法时的GLSurfaceView几何形状的变化,包括尺寸变化GLSurfaceView或设备屏幕的取向。例如,当设备从纵向变为横向的系统调用这个方法。使用此方法可以在变化做出反应GLSurfaceView容器。
介绍完了GlSurfaceView和GlSurfaceView.renderer之后,接下来说下如何使用GlSurfaceView;
1. 创建一个GlSurfaceView
2. 为这个GlSurfaceView设置渲染
3. 在GlSurfaceView.renderer中绘制处理显示数据
5. OpenGl的简单使用实例(绘制一个三角形)
在使用OpenGl之前,需要在AndroidManifest.xml中设置OpenGl的版本:这里我们使用的是OpenGl ES 2.0,所以需要添加如下说明:
1
|
< uses-feature android:glEsVersion = "0x00020000" android:required = "true" />
|
使用GLSufaceView(上面有介绍)
-
具体在GlSurfaceView.Renderer中的绘制步骤:
-
设置视图展示窗口(viewport) :在onSurfaceChanged中调用GLES20.glViewport(0, 0, width, height);
-
创建图形类,确定好顶点位置和图形颜色,将顶点和颜色数据转换为OpenGl使用的数据格式
-
加载顶点找色器和片段着色器用来修改图形的颜色,纹理,坐标等属性
-
创建投影和相机视图来显示视图的显示状态,并将投影和相机视图的转换传递给着色器。
-
创建项目(Program),连接顶点着色器片段着色器。
-
将坐标数据传入到OpenGl ES程序中:
使用OpenGl修改背景颜色
创建一个GlSurfaceView,并为其设置渲染OneGlRenderer;
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class OneGlSurfaceView extends GLSurfaceView {
private final OneGlRenderer mRenderer;
public OneGlSurfaceView(Context context) {
super (context);
setEGLContextClientVersion( 2 );
mRenderer = new OneGlRenderer();
setRenderer(mRenderer);
}
}
|
实现渲染接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class OneGlRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor( 0 .0f, 0 .0f, 0 .0f, 1 .0f);
}
public void onDrawFrame(GL10 unused) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport( 0 , 0 , width, height);
}
}
|
展示渲染后的GlSurfaceView
1
2
3
4
5
6
7
8
9
|
public class OneOpenGlActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
OneGlSurfaceView glSurfaceView = new OneGlSurfaceView( this );
setContentView(glSurfaceView);
}
}
|
效果如下:就是简单给GlSurfaceView渲染一层黑色。
![](https://www.f11.cn/uploads/allimg/200316/131549B03-0.png)
使用OpenGl绘制几何图形
一:图形创建
创建一个几何图形(这里主要列举三角形和正方形),需要注意一点,我们设置图形的顶点坐标后,需要将顶点坐标转为ByteBuffer,这样OpenGl才能进行图形处理。
三角形图形创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class Triangle {
private FloatBuffer vertexBuffer;
static final int COORDS_PER_VERTEX = 3 ;
static float triangleCoords[] = {
0 .0f, 0 .5f, 0 .0f,
- 0 .5f, - 0 .5f, 0 .0f,
0 .5f, - 0 .5f, 0 .0f
};
float color[] = { 255 , 0 , 0 , 1 .0f };
public Triangle() {
ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4 );
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(triangleCoords);
vertexBuffer.position( 0 );
}
}
|
正方型图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
static final int COORDS_PER_VERTEX = 3 ;
static float squareCoords[] = {
- 0 .5f, 0 .5f, 0 .0f,
- 0 .5f, - 0 .5f, 0 .0f,
0 .5f, - 0 .5f, 0 .0f,
0 .5f, 0 .5f, 0 .0f };
private short drawOrder[] = { 0 , 1 , 2 , 0 , 2 , 3 };
public Square() {
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4 );
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position( 0 );
ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2 );
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position( 0 );
}
}
|
创建图形基本没什么技巧可言,按部就班就行了,为什么数据需要转换格式呢?主要是因为Java的缓冲区数据存储结构为大端字节序(BigEdian),而OpenGl的数据为小端字节序(LittleEdian),因为数据存储结构的差异,所以,在Android中使用OpenGl的时候必须要进行下转换。当然,一般我们在使用的时候都会做个简单的工具类。这里提供几个简单的封装。(占几个字节就初始化ByteBuffer长度的时候*几)
将int[]转成IntBuffer
1
2
3
4
5
6
7
8
9
10
11
12
|
private IntBuffer intBufferUtil( int [] arr)
{
IntBuffer mBuffer;
ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4 );
qbb.order(ByteOrder.nativeOrder());
mBuffer = qbb.asIntBuffer();
mBuffer.put(arr);
mBuffer.position( 0 );
return mBuffer;
}
|
将float[]数组转为OpenGl 所需要的FloatBuffer
1
2
3
4
5
6
7
8
9
10
11
12
|
private FloatBuffer floatBufferUtil( float [] arr)
{
FloatBuffer mBuffer;
ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4 );
qbb.order(ByteOrder.nativeOrder());
mBuffer = qbb.asFloatBuffer();
mBuffer.put(arr);
mBuffer.position( 0 );
return mBuffer;
}
|
当然,依葫芦画瓢,如何将short[]转ShortBuffer这个就照着写就ok了
1
2
3
4
5
6
7
8
9
10
11
12
|
private ShortBuffer shortBufferUtil( short [] arr){
ShortBuffer mBuffer;
ByteBuffer dlb = ByteBuffer.allocateDirect(
arr.length * 2 );
dlb.order(ByteOrder.nativeOrder());
mBuffer = dlb.asShortBuffer();
mBuffer.put(arr);
mBuffer.position( 0 );
return mBuffer;
}
|
创建完形状之后,我们就要进行我们的第二步了,将这些形状渲染到GlSurfaceView中去。主要可分为下面几步:
1. 首先我们需要在GlSurfaceView.Renderer中初始化需要渲染的几何图形
1
2
3
4
5
6
7
8
9
10
|
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor( 0 .0f, 0 .0f, 0 .0f, 1 .0f);
mTriangle = new Triangle();
mSquare = new Square();
}
|
二.:绘制图形,因为需要提供很多细节的图形渲染管线,所以绘制图形前至少需要一个顶点着色器来绘制形状和一个片段着色器的颜色,形状。这些着色器必须被编译,然后加入到一个OpenGL ES程序,然后将其用于绘制形状。简单介绍下这几个概念:
- 顶点着色器(Vertex Shader)顶点着色器是GPU上运行的小程序,由名字可以知道,通过它来处理顶点,他用于渲染图形顶点的OpenGL ES图形代码。顶点着色器可用来修改图形的位置,颜色,纹理坐标,不过不能用来创建新的顶点坐标。
- 片段着色器(Fragment Shader ) 用于呈现与颜色或纹理的形状的面的OpenGL ES代码。
- 项目(Program) -包含要用于绘制一个或多个形状着色器的OpenGL ES的对象。
下面给Triangle类定义一个基本的着色器代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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;" +
"}" ;
...
}
|
当然,上面我们创建了着色器的编译代码,代码编写完成,需要写个方法来执行这段代码,这里我们在渲染器中写一个如下方法来执行着色器代码:
1
2
3
4
5
6
7
8
9
10
|
public static int loadShader( int type, String shaderCode){
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
|
这里有一点需要注意,因为着色器的代码执行是很昂贵滴,所以避免多次执行,需要我们一般将执行代码的逻辑写带图形类的构造方法中。比如上面的Triangle,我们就这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private final int mProgram;
public Triangle() {
... ...
int vertexShader = OneGlRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = OneGlRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}
|
最后,所有绘制的所有基本配置都配置完成之后,我们来写绘制图形的方法,我们在图形类(Triangle)中创建一个绘制的方法onDraw(),可以在onDraw()方法中设置绘制逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4 ;
public void draw() {
GLES20.glUseProgram(mProgram);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition" );
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false ,
vertexStride, vertexBuffer);
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor" );
GLES20.glUniform4fv(mColorHandle, 1 , color, 0 );
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0 , vertexCount);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
|
完成上面所有步骤,只需要在GlSurfaceView.Renderer的onDrawFrame()方法中调用图形类的绘制方法即可(上面的onDraw()):
1
2
3
|
public void onDrawFrame(GL10 unused) {
mTriangle.draw();
}
|
最后的呈现效果如下图所示:
![](https://www.f11.cn/uploads/allimg/200316/131549BZ-1.png)
运用投影和相机视图
通常情况下,OpenGl中展示的视图和在Android上显示的图形会有偏差。借用官方图片:
-[-/a>
当然我们可以通过矩阵转换来解决这种问题,让OpenGl上的视图在任何android设备上显示的比例都是一样的,这里说下什么是投影和相机视图:
投影的定义
使用OpenGl绘制的3D图形,需要展示在移动端2D设备上,这就是投影。Android OpenGl ES中有两种投影方式:一种是正交投影,一种是透视投影:
正交投影投影物体的带下不会随观察点的远近而发生变化,我们可以使用下面方法来执行正交投影:
1
2
3
4
5
6
7
8
|
Matrix.orthoM ( float [] m,
int mOffset,
float left,
float right,
float bottom,
float top,
float near,
float far)
|
透视投影:随观察点的距离变化而变化,观察点越远,视图越小,反之越大,我们可以通过如下方法来设置透视投影:
1
2
3
4
5
6
7
8
|
Matrix.frustumM ( float [] m,
int mOffset,
float left,
float right,
float bottom,
float top,
float near,
float far)
|
相机视图
什么是相机视图?简单来说生活中我们拍照,你站的高度,拿相机的位置,姿势不同,拍出来的照片也就不一样,相机视图就是来修改相机位置,观察方式以及相机的倾斜角度等属性。我们可以通过下面方法来修改相机视图属性:
1
2
3
4
5
|
Matrix.setLookAtM ( float [] rm,
int rmOffset,
float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ)
|
转换矩阵(变换矩阵)
转换矩阵用来做什么的呢?是否记得上面我们绘制的图形坐标需要转换为OpenGl中能处理的小端字节序(LittleEdian),没错,转换矩阵就是用来将数据转为OpenGl ES可用的数据字节,我们将相机视图和投影设置的数据相乘,便得到一个转换矩阵,然后我们再讲此矩阵传给顶点着色器,具体使用方法及参数说明如下:
1
2
3
4
5
6
|
Matrix.multiplyMM ( float [] result,
int resultOffset,
float [] lhs,
int lhsOffset,
float [] rhs,
int rhsOffset)
|
下面简单讲解下如何使用投影和相机视图来实现矩阵变换并传递给顶点着色器;
定义一个投影:
1
2
3
4
5
6
7
8
9
10
11
12
|
private final float [] mMVPMatrix = new float [ 16 ];
private final float [] mProjectionMatrix = new float [ 16 ];
private final float [] mViewMatrix = new float [ 16 ];
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport( 0 , 0 , width, height);
float ratio = ( float ) width / height;
Matrix.frustumM(mProjectionMatrix, 0 , -ratio, ratio, - 1 , 1 , 3 , 7 );
}
|
定义一个相机视图
1
2
3
4
5
6
7
8
9
10
11
12
|
@Override
public void onDrawFrame(GL10 unused) {
...
Matrix.setLookAtM(mViewMatrix, 0 , 0 , 0 , - 3 , 0f, 0f, 0f, 0f, 1 .0f, 0 .0f);
Matrix.multiplyMM(mMVPMatrix, 0 , mProjectionMatrix, 0 , mViewMatrix, 0 );
mTriangle.draw(mMVPMatrix);
}
|
修改图形类执行代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class Triangle {
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
"}" ;
private int mMVPMatrixHandle;
...
}
|
投影和相机视图代码到图形类的绘制方法中去onDraw()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public void draw( float [] mvpMatrix){
... ...
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix" );
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1 , false , mvpMatrix, 0 );
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0 , vertexCount);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
|
做完这些,我们就能得到如下图:
![](https://www.f11.cn/uploads/allimg/200316/13154a630-3.png)
没错,这才没有变形的视图。到这里,基本的通过OpenGl绘制简单图形就over了,下面我们讲解下如何添加一些交互动作。
添加动作
前面都是简单的动作介绍,使用OpenGl在屏幕上绘制对象是使用openGl的基本功。下面我来说下如何添加旋转形状。使用OpenGl的描绘对象是相对简单的,首先需要在渲染器中创建一组旋转矩阵,然后使用之前提到过的投影和相机视图变换矩阵结合起来使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
private float [] mRotationMatrix = new float [ 16 ];
public void onDrawFrame(GL10 gl) {
float [] scratch = new float [ 16 ];
...
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0 .090f * (( int ) time);
Matrix.setRotateM(mRotationMatrix, 0 , angle, 0 , 0 , - 1 .0f);
Matrix.multiplyMM(scratch, 0 , mMVPMatrix, 0 , mRotationMatrix, 0 );
mTriangle.draw(scratch);
}
|
运行效果图如下:
![](https://www.f11.cn/uploads/allimg/200316/1315493H7-4.gif)
修改顶点颜色
一个颜色是不是太单调了?如何让做成多彩的呢?接下来我们来做一个多彩三角形,如何来做一个多彩三角形?我们通过顶点着色器来做。基于上面的代码,我们只需要做一点点改动,下面是基本步骤:
1. 修改着色器代码
2. 将颜色值修改为float数组并转为floatBuffer
3. 将获取的floatBuffer传递给顶点着色器。
修改着色器代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"uniform mat4 uMVPMatrix;" +
"varying vec4 vColor;" +
"attribute vec4 aColor;" +
"void main() {" +
" gl_Position = uMVPMatrix*vPosition;" +
" vColor=aColor;" +
"}" ;
private final String fragmentShaderCode =
"precision mediump float;" +
"varying vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}" ;
|
shader的变量类型(uniform,attribute和varying)的区别
关于shader的变量类型(uniform,attribute和varying)的区别及使用,下面做下说明:
1. uniform:uniform变量在vertex和fragment两者之间声明方式完全一样,则它可以在vertex和fragment共享使用。(相当于一个被vertex和fragment shader共享的全局变量)uniform变量一般用来表示:变换矩阵,材质,光照参数和颜色等信息。在代码中通过GLES20.glGetUniformLocation(int program, String name)来获取属性值。并通过 GLES20.glUniformMatrix4fv(int location, int count, boolean transpose, float[] value, int offset);方法将数据传递给着色器。
2. attribute:这个变量只能在顶点着色器中使用(vertex Shader),用来表示顶点的数据,比如顶点坐标,顶点颜色,法线,纹理坐标等。在绘制的时候通过GLES20.glGetAttribLocation(int program, String name)来获取变量值,通过 GLES20.glEnableVertexAttribArray(int index)来启动句柄,最后通过 GLES20.glVertexAttribPointer(int indx,int size,int type,boolean normalized,int stride,java.nio.Buffer ptr)来设置图形数据。
3. varying变量:这个变量只能用来在vertex和fragment shader之间传递数据时使用,不可以通过代码获取其变量值。
接来下我们进行数据转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
float color[] = {
1 .0f, 0f, 0f, 1 .0f ,
0f, 1 .0f, 0f, 1 .0f ,
0f, 0f, 1 .0f, 1 .0f
};
public Triangle() {
... ...
ByteBuffer dd = ByteBuffer.allocateDirect(
color.length * 4 );
dd.order(ByteOrder.nativeOrder());
colorBuffer = dd.asFloatBuffer();
colorBuffer.put(color);
colorBuffer.position( 0 );
}
|
最后我们需要获取着色器的句柄并设置着色器的颜色:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public void draw( float [] mvpMatrix){
... ...
mColorHandle = GLES20.glGetAttribLocation(mProgram, "aColor" );
GLES20.glEnableVertexAttribArray(mColorHandle);
GLES20.glVertexAttribPointer(mColorHandle, 4 ,
GLES20.GL_FLOAT, false ,
0 ,colorBuffer);
... ...
}
|
![](https://www.f11.cn/uploads/allimg/200316/131549D57-5.gif)
6. 参考链接:
7. 项目地址:
此项目为博主所有的系列学习的代码汇总项目,该文章的代码位于:opengl/OneOpenGl/OneOpenGlActivity
|