安卓 OpenGL ES 2.0 完全入门(一):基本概念和 hello world
2018-03-29 10:47
453 查看
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2016/06/07/Open-gl-es-android-2-part-1/做安卓开发满打满算也有 3 年了,OpenGL 这块之前完全没有涉及过,这两周一直在整理安卓相机预览、用 GPUImage 进行美颜处理以及美颜后的数据传输这块内容,结果 GPUImage 的美颜原理基本一窍不通,因此就把 OpenGL ES 好好入了个门,并且整理为 安卓 OpenGL ES 2.0 完全入门 系列。本文是系列第一篇,主要是介绍了 OpenGL 的一些基本概念,并且包含了对一个 hello world 程序的完全解析,注意,并不是有一个 hello world,而是对其进行了完全解析!Update 2017.07.16:时隔一年,工作再次涉及安卓平台 OpenGL 相关内容,趁此机会又学习了一次基础知识,并对本文做了更新。
接下来介绍 Shader(着色器程序):Shader 用来描述如何绘制(渲染),GLSL 是 OpenGL 的编程语言,全称 OpenGL Shader Language,它的语法类似于 C 语言。OpenGL 渲染需要两种 Shader:Vertex Shader 和 Fragment Shader。每个 Vertex 都会执行一遍 Vertex Shader,以确定 Vertex 的最终位置,其 main 函数中必须设置
4000
ent Shader,以确定每个 Fragment 的颜色,其 main 函数中必须设置
下面对图中的几个概念作简单说明:Local space:我们为每个物体建好模型的时候,它们的坐标就是 Local space 坐标;
World space:当我们要绘制多个物体时,如果直接使用 Local space 的坐标(把所有物体的原点放在一起),那它们很可能会发生重叠,因此我们需要把它们进行合理的移动、排布,最终各自的坐标就是 World space 的坐标了;
Model matrix:把 Local space 坐标转换到 World space 坐标所使用的变换矩阵,它是针对每个物体做不同的变换;
View space:通常也叫 Camera space 或者 Eye space,是从观察者(也就是我们自己)所在的位置出发,所看到的空间;
View matrix:把 World space 坐标转换到 View space 坐标所使用的变换矩阵,它相当于是在移动相机位置,实际上是反方向移动整个场景(所有物体);
Clip space:OpenGL 只会渲染坐标值范围在
Projection matrix:把 View space 坐标转换到 Clip space 坐标所使用的变换矩阵,它会指定一个可见的范围,只有这个范围内的点才会转换到 NDC 中,而这个范围被称作视锥(frustum);projection matrix 有三种创建方式:正投影(Orthographic projection),透视投影(Perspective projection),以及 3D 投影(3D projection);前两种比较常用;
Screen space:屏幕上的空间,
Viewport transform:这一步是 Open GL 自动完成的,把 Clip space 坐标转换到 Screen space 坐标;
通常说的 Model,View,Projection 这三种变换都是针对 Vertex 坐标做的变换,也就是:
更多关于坐标系的内容,可以阅读 Learn OpenGL 的 Coordinate Systems 部分。
3.2.
Renderer 包含三个接口:
加载 shader 代码:
attatch shader 代码:
链接 GLSL 程序:
使用 GLSL 程序:
获取 shader 代码中的变量索引:
启用 vertex:
绑定 vertex 坐标值:
需要指出的是,我们的 Java 代码需要获取 shader 代码中定义的变量索引,用于在后面的绘制代码中进行赋值,变量索引在 GLSL 程序的生命周期内(链接之后和销毁之前),都是固定的,只需要获取一次。
它们的 z 轴坐标都是 0,所以也就是上面图一的效果了。
4.1.
perspectiveM 就是我们所说的透视投影了,我们只需要提供几个参数,就可以得到投影矩阵,用于投影变换了。下面我将详细分析每个参数的含义:
安卓 OpenGL ES 2.0 完全入门(三):2D 纹理的裁剪、翻转、旋转、缩放
WebRTC-Android 源码导读(二):预览实现分析
1. 基本概念
在 OpenGL 的世界里,我们只能画点、线、三角形,复杂的图形都是由三角形构成的。在 OpenGL 里有两个最基本的概念:Vertex 和 Fragment。一切图形都从 Vertix 开始,Vertix 序列围成了一个图形。那什么是 Fragment 呢?为此我们需要了解一下光栅化(Rasterization):光栅化是把点、线、三角形映射到屏幕上的像素点的过程(每个映射区域叫一个 Fragment),也就是生成 Fragment 的过程。通常一个 Fragment 对应于屏幕上的一个像素,但高分辨率的屏幕可能会用多个像素点映射到一个 Fragment,以减少 GPU 的工作。接下来介绍 Shader(着色器程序):Shader 用来描述如何绘制(渲染),GLSL 是 OpenGL 的编程语言,全称 OpenGL Shader Language,它的语法类似于 C 语言。OpenGL 渲染需要两种 Shader:Vertex Shader 和 Fragment Shader。每个 Vertex 都会执行一遍 Vertex Shader,以确定 Vertex 的最终位置,其 main 函数中必须设置
gl_Position全局变量,它将作为该 Vertex 的最终位置,进而把 Vertex 组合(assemble)成点、线、三角形。光栅化之后,每个 Fragment 都会执行一次 Fragm
4000
ent Shader,以确定每个 Fragment 的颜色,其 main 函数中必须设置
gl_FragColor全局变量,它将作为该 Fragment 的最终颜色。下面是 OpenGL 的处理过程:
2. 坐标系
弄清楚坐标系很重要,不然会找不着东南西北。下面这张图展示了 OpenGL 通常的处理流程中各个环节的坐标系,以及坐标系之间的转换操作:下面对图中的几个概念作简单说明:Local space:我们为每个物体建好模型的时候,它们的坐标就是 Local space 坐标;
World space:当我们要绘制多个物体时,如果直接使用 Local space 的坐标(把所有物体的原点放在一起),那它们很可能会发生重叠,因此我们需要把它们进行合理的移动、排布,最终各自的坐标就是 World space 的坐标了;
Model matrix:把 Local space 坐标转换到 World space 坐标所使用的变换矩阵,它是针对每个物体做不同的变换;
View space:通常也叫 Camera space 或者 Eye space,是从观察者(也就是我们自己)所在的位置出发,所看到的空间;
View matrix:把 World space 坐标转换到 View space 坐标所使用的变换矩阵,它相当于是在移动相机位置,实际上是反方向移动整个场景(所有物体);
Clip space:OpenGL 只会渲染坐标值范围在
[-1, 1]的内容,超出这个范围的内容都会被裁剪掉,这个范围的空间就叫 Clip space,Clip space 的坐标系也叫 normalized device coordinate(NDC);
Projection matrix:把 View space 坐标转换到 Clip space 坐标所使用的变换矩阵,它会指定一个可见的范围,只有这个范围内的点才会转换到 NDC 中,而这个范围被称作视锥(frustum);projection matrix 有三种创建方式:正投影(Orthographic projection),透视投影(Perspective projection),以及 3D 投影(3D projection);前两种比较常用;
Screen space:屏幕上的空间,
glViewport调用指定的区域;
Viewport transform:这一步是 Open GL 自动完成的,把 Clip space 坐标转换到 Screen space 坐标;
通常说的 Model,View,Projection 这三种变换都是针对 Vertex 坐标做的变换,也就是:
更多关于坐标系的内容,可以阅读 Learn OpenGL 的 Coordinate Systems 部分。
3. Hello world
先看一下最简单的完整例子(下文有详细分析,完整代码可以在 GitHub 获取),绘制一个三角形,效果如图一:public class MainActivity extends AppCompatActivity { private GLSurfaceView mGLSurfaceView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (!Utils.supportGlEs20(this)) { Toast.makeText(this, "GLES 2.0 not supported!", Toast.LENGTH_LONG).show(); finish(); return; } mGLSurfaceView = (GLSurfaceView) findViewById(R.id.surface); mGLSurfaceView.setEGLContextClientVersion(2); mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); mGLSurfaceView.setRenderer(new MyRenderer()); mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); } @Override protected void onPause() { super.onPause(); mGLSurfaceView.onPause(); } @Override protected void onResume() { super.onResume(); mGLSurfaceView.onResume(); } private static class MyRenderer implements GLSurfaceView.Renderer { private static final String VERTEX_SHADER = "attribute vec4 vPosition;\n" + "void main() {\n" + " gl_Position = vPosition;\n" + "}"; private static final String FRAGMENT_SHADER = "precision mediump float;\n" + "void main() {\n" + " gl_FragColor = vec4(0.5, 0, 0, 1);\n" + "}"; private static final float[] VERTEX = { // in counterclockwise order: 0, 1, 0, // top -0.5f, -1, 0, // bottom left 1, -1, 0, // bottom right }; private final FloatBuffer mVertexBuffer; private int mProgram; private int mPositionHandle; MyRenderer() { mVertexBuffer = ByteBuffer.allocateDirect(VERTEX.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(VERTEX); mVertexBuffer.position(0); } static int loadShader(int type, String shaderCode) { int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); return shader; } @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); mProgram = GLES20.glCreateProgram(); int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER); GLES20.glAttachShader(mProgram, vertexShader); GLES20.glAttachShader(mProgram, fragmentShader); GLES20.glLinkProgram(mProgram); GLES20.glUseProgram(mProgram); mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 12, mVertexBuffer); } @Override public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); } @Override public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3); } } }
3.1. set up
首先我们需要一个GLSurfaceView,它是让我们渲染的“画布”。然后我们需要一个
GLSurfaceView.Renderer,它将实现我们的渲染逻辑。此外我们还将设置 GL ES 版本,并将 GLSurfaceView 和 Renderer 连接起来:
mGLSurfaceView.setEGLContextClientVersion(2); mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); mGLSurfaceView.setRenderer(new MyRenderer()); mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);RenderMode 有两种,
RENDERMODE_WHEN_DIRTY和
RENDERMODE_CONTINUOUSLY,前者是懒惰渲染,需要手动调用
glSurfaceView.requestRender()才会进行更新,而后者则是不停渲染。
3.2. GLSurfaceView.Renderer
Renderer 包含三个接口:public interface Renderer { void onSurfaceCreated(GL10 gl, EGLConfig config); void onSurfaceChanged(GL10 gl, int width, int height); void onDrawFrame(GL10 gl); }
onSurfaceCreated在 surface 创建时被回调,通常用于进行初始化工作,只会被回调一次;
onSurfaceChanged在每次 surface 尺寸变化时被回调,注意,第一次得知 surface 的尺寸时也会回调;
onDrawFrame则在绘制每一帧的时候回调。
3.3. GLSL 程序
和普通的 view 利用 canvas 来绘制不一样,OpenGL 需要加载 GLSL 程序,让 GPU 进行绘制。所以我们需要定义 shader 代码,并在初始化时(也就是onSurfaceCreated回调中)加载:
@Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); mProgram = GLES20.glCreateProgram(); int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER); GLES20.glAttachShader(mProgram, vertexShader); GLES20.glAttachShader(mProgram, fragmentShader); GLES20.glLinkProgram(mProgram); GLES20.glUseProgram(mProgram); mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 12, mVertexBuffer); } static int loadShader(int type, String shaderCode) { int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); return shader; }GLSL 的语法并不是本文的主要内容,这里就不深入展开了。创建 GLSL 程序:
glCreateProgram
加载 shader 代码:
glShaderSource和
glCompileShader
attatch shader 代码:
glAttachShader
链接 GLSL 程序:
glLinkProgram
使用 GLSL 程序:
glUseProgram
获取 shader 代码中的变量索引:
glGetAttribLocation
启用 vertex:
glEnableVertexAttribArray
绑定 vertex 坐标值:
glVertexAttribPointer
需要指出的是,我们的 Java 代码需要获取 shader 代码中定义的变量索引,用于在后面的绘制代码中进行赋值,变量索引在 GLSL 程序的生命周期内(链接之后和销毁之前),都是固定的,只需要获取一次。
3.4. 设置 Screen space 的大小
我们可以利用glViewport设置 Screen space 的大小,通常在
onSurfaceChanged中调用:
@Override public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); }
3.5. 绘制
我们在onDrawFrame回调中执行绘制操作,绘制的过程其实就是为 shader 代码变量赋值,并调用绘制命令的过程:
@Override public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3); }由于顶点坐标已经绑定过了,所以这里无需进行变量赋值,直接调用绘制指令即可。我们可以通过
GLES20.glDrawArrays或者
GLES20.glDrawElements开始绘制。注意,执行完毕之后,GPU 就在显存中处理好帧数据了,但此时并没有更新到 surface 上,是
GLSurfaceView会在调用
renderer.onDrawFrame之后,调用
eglSwapBuffers,来把显存的帧数据更新到 surface 上的。这里我们绘制的是一个三角形,OpenGL 坐标原点在屏幕中心,三个顶点分别是:
(0, 1, 0),位于屏幕顶部中心点;
(-0.5f, -1, 0),位于屏幕底部四分之一点;
(1, -1, 0),位于屏幕右下角;
它们的 z 轴坐标都是 0,所以也就是上面图一的效果了。
4. 投影变换
你肯定已经注意到,OpenGL 坐标系和安卓手机坐标系不是线性对应的,因为手机的宽高比几乎都不是 1。因此我们绘制的形状是变形的,怎么解决这个问题呢?答案是投影变换(projection)。前面我们已经知道,投影变换用于把 View space 的坐标转换为 Clip space 的坐标,在这个转换过程中,它还能顺带处理宽高比的问题。使用较多的是正投影和透视投影,这里我们使用透视投影:Matrix.perspectiveM。通常坐标系的变换都是对顶点坐标进行矩阵左乘运算,因此我们需要修改我们的 vertex shader 代码:
private static final String VERTEX_SHADER = "attribute vec4 vPosition;\n" + "uniform mat4 uMVPMatrix;\n" + "void main() {\n" + " gl_Position = uMVPMatrix * vPosition;\n" + "}";然后我们需要在
onSurfaceCreated中获取
uMVPMatrix的索引:
@Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { // ... mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); mMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); // ... }并在
onSurfaceChanged中计算变换矩阵:
@Override public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); Matrix.perspectiveM(mMVPMatrix, 0, 45, (float) width / height, 0.1f, 100f); Matrix.translateM(mMVPMatrix, 0, 0f, 0f, -2.5f); }
Matrix.perspectiveM,
Matrix.translateM?先别急,我将在下文进行详细解释。最后我们在绘制的时候为
uMVPMatrix赋值:
@Override public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glUniformMatrix4fv(mMatrixHandle, 1, false, mMVPMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3); }经过这样的变换之后,绘制的效果如图二:
4.1. perspectiveM
和 translateM
perspectiveM 就是我们所说的透视投影了,我们只需要提供几个参数,就可以得到投影矩阵,用于投影变换了。下面我将详细分析每个参数的含义:public static void perspectiveM(float[] m, int offset, float fovy, float aspect, float zNear, float zFar) Matrix.perspectiveM(mMVPMatrix, 0, 45, (float) width / height, 0.1f, 100f);前两个参数不用多说,Javadoc 里面就有,
m是保存变换矩阵的数组,
offset是开始保存的下标偏移量。fovy 是 y 轴的 field of view 值,也就是视角大小,视角越大,我们看到的范围就越广,例如下面的 90° 和 45°:
aspect是 Screen space 的宽高比。
zNear和
zFar则是视锥体近平面和远平面的 z 轴坐标了。由于历史原因,
Matrix.perspectiveM会让 z 轴方向倒置,所以左乘投影矩阵之后,顶点 z 坐标需要在
-zNear~-zFar范围内才会可见。前面我们顶点的 z 坐标都是 0,我们可以把它修改为
-0.1f~-100f之间的值,也可以通过一个位移变换来达到此目的:
public static void translateM( float[] m, int mOffset, float x, float y, float z) Matrix.translateM(mMVPMatrix, 0, 0f, 0f, -2.5f);我们沿着 z 轴的反方向移动 2.5,这样就能把 z 坐标移到
-0.1f~-100f了。
5. 小结
讲完 hello world 文章有已经这么长了,所以其他的还是放到后面的部分吧,包括:渲染矩形、渲染图片纹理、读取显存数据(保存为图片或者网络传输)、GLSL 简介等,GLSL 可能不会太深入,不要太期待 :)后续文章
安卓 OpenGL ES 2.0 完全入门(二):矩形、图片、读取显存等安卓 OpenGL ES 2.0 完全入门(三):2D 纹理的裁剪、翻转、旋转、缩放
WebRTC-Android 源码导读(二):预览实现分析
相关文章推荐
- 安卓 OpenGL ES 2.0 完全入门(一):基本概念和 hello world
- OpenGL ES 2.0基本概念
- 博弈论入门:一、博弈论基本概念及完全信息静态博弈
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- OpenGL ES 2.0基本概念
- 基于服务的企业集成模式轻松入门,第 2 部分: 进一步介绍基本概念的演变
- Java新手入门的30个基本概念
- 从零开始学习OpenGL ES之一 – 基本概念
- LaTeX新人教程,30分钟从完全陌生到基本入门
- Android OpenGL ES 简明开发教程三:3D绘图基本概念
- GIT入门笔记(1)- Git的基本概念
- PC-Lint的入门-- 一、概念与基本操作
- GCD入门(一): 基本概念和Dispatch Queue
- JMS入门基本概念