OpenGL.ES在Android上的简单实践:1-曲棍球(基本环境和定义顶点)
2018-01-09 16:37
316 查看
OpenGL.ES在Android上的简单实践:1-曲棍球(基本环境和定义顶点)
简单的曲棍球 实例编码 1废话不说,开码。
1、首先创建一个空的Activity,命名HockeyActivity。去除默认的setContentView,我们不用自定义布局文件,增加两个成员变量。
public class HockeyActivity extends Activity { private GLSurfaceView glSurfaceView; private boolean rendererSet = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); glSurfaceView = new GLSurfaceView(this) } }
2、检查系统是否支持OpenGL.ES 2.0
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo deviceConfigurationInfo = activityManager.getDeviceConfigurationInfo(); final boolean supportEs2 = deviceConfigurationInfo.reqGlEsVersion >= 0x20000;
但是这还不够,因为模拟器上的GPU部分是有缺陷的,为了使代码在模拟器上正常工作,需要加上以下检查条件 (开发的时候肯定是用真机比较好的,但是模拟器方便截图啊!)
final boolean supportsEs2 = glVersion >= 0x20000 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 && (Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86")))
3、下一步就是配置渲染器
if(supportEs2 ) { // 指定EGL版本 glSurfaceView.setEGLContextClientVersion(2); // 指定渲染器 glSurfaceView.setRenderer(new HockeyRenderer()); rendererSet = true; } else { Toast.makeText(this, "该设备不支持OpenGL.ES 2.0",Toast.LENGTH_SHORT).show(); return; }
通过设备支持验证后,我们就通过调用setEGLContextClientVersion确定EGL的版本,然后调用setRenderer 传入自定义的Renderer类实例,稍后我们分析这个Renderer类是什么(重点类)同时这段代码通过设置rendererSet为true,记住渲染器renderer已经设置过了。 然后,我们设置glSurfaceView为我们主界面的视图
setContentView(glSurfaceView);
到这,还不能放松,我们必须处理好Activity和glSurfaceView的生命周期事件。
@Override protected void onResume() { super.onResume(); if( rendererSet ){ glSurfaceView.onResume(); } } @Override protected void onPause() { super.onPause(); if( rendererSet ){ glSurfaceView.onPause(); } }
这些方法非常重要,有了它们,整个GLSurfaceView视图才能正确暂停并继续后台渲染线程,同时释放和续用OpenGL上下文,如果没有做这些,应用程序可能会崩溃,并被Android系统终止,详细原因这里源码分析;我们还要保证渲染器也被设置(setRenderer 被调用 && rendererSet==true),否则调用这些方法生命周期方法会引起renderer==null的崩溃。
紧接着就是去实现Renderer,我们命名为HockeyRenderer,其中我们分别在三个接口中各添加一句代码,
public class HockeyRenderer implements GLSurfaceView.Renderer { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES20.glClearColor(1.0f, 0.0f, 0.0f, 0.0f); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { GLES20.glViewport(0,0,width,height); } @Override public void onDrawFrame(GL10 gl) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); } }
onSurfaceCreated(GL10 gl, EGLConfig config) 当Surface被创建的时候,GLSurfaceView会调用这个方法;这发生在应用程序第一次运行的时候,并且当设备长时间休眠,系统回收资源后重新被唤醒,这个方法也可能会被调用。这意味着,本方法可能会被调用多次。 我们调用GLES20.glClearColor(float red, float green, float blue, float alpha); 设置清空屏幕用的颜色;前三个参数分别对应红,绿和蓝,最后的参数对应透明度。
onSurfaceChanged(GL10 gl, int width, int height) 在Surface被创建以后,每次Surface尺寸变化时,在横竖屏切换的时候,这个方法都会被调用到。 我们调用 GLES20.glViewport(int x, int y, int width, int height); 设置视口的尺寸,这个视口尺寸怎么理解呢,就是锁定你操作的渲染区域是哪部分,整个屏幕那就是 (0.0)点开始,宽度为widht,长度为hight咯。如果你只想渲染左半个屏幕,那就(0.0),宽度width/2,长度为hight/2。这样设置viewport大小后,你之后的GL画图操作,都只作用这部分区域,右半屏幕是不会有任何反应的。( 连带思考VR的左右屏??? ^_^)
onDrawFrame(GL10 gl) 当绘制一帧时,这个方法会被调用。在这个方法中,我们一定要绘制一些东西,即使只是清空屏幕;因为在这方法返回之后,渲染缓冲区会被交换(前人的源码分析),并显示在屏幕上,如果什么都没画,可能会看到糟糕的闪烁效果。 我们调用 GLES20.glClear(GL_COLOR_BUFFER_BIT); 清空屏幕,这会擦除屏幕上的所有颜色,并用之前glClearColor调用定义的颜色填充整个屏幕。
怎么会有一个未被使用的参数类型GL10呢?它是OpenGL.ES 1.0的API遗留下来的,我们使用OpenGL.ES 2.0。所以直接忽略就可以了,GLES20类提供了静态方法来存取。 那么问题又来了,这些GLES20方法为啥全是静态, 为啥不整个包导入进项目,次次写麻烦? 除了GLES20好像还有很多类似的GLES10,GLES10Ext,GLES11,GLES11Ext,GLES30,GLES30Ext,GLES31。 从版本上我们可以知道,Android系统的接口已经升级到3.1的版本了,而且还有一些Android独有的扩展(Ext),我们大可以不用太纠结于这些版本的抬头,它们在写程序的时候都是可以通用的,不过要注意最高版本的限制,我们在开始设置setEGLContextClientVersion( 2 ),所以我们能用GLES10,GLES10Ext,GLES11,GLES11Ext,GLES20。但不能使用GLES30,GLES30Ext,GLES31了,就算你代码上写了,编译不报错,但是程序运行是不会有对应的效果的。 至于为啥写函数都带上静态类名,这是容易让我们区分该特性接口是哪个版本的内容。 大家嫌弃麻烦就导入静态包都可以的。
在幕后,GLSurfaceView实际上为它自己创建了一个窗口(window),并在视图层次(View Hierarchy)上穿了个“洞”,让底层的OpenGL surface显示出来。对于大多数使用情况,这足够了;但是,GLSurfaceView与常规视图(view)不同,它没有动画或者变形特效,因为GLSurfaceView是窗口(window)的一部分。
从Android 4.0开始,Android提供了一个纹理视图(TextureView),它可以渲染OpenGL而不用创建单独的窗口或打洞了,这意味着,这个视图像一个常规视图(view)一样,可以被操作,且有动画和变形特效。但是,TextureView类没有内置OpenGL初始化操作,要想使用TextureView,一种方法是执行自定义的OpenGL初始化并在TextureView上运行,另外一种方法是把GLSurfaceView的源代码拿出来,把它适配到TextureView上。这里 还真有网友做出来,但是并不完善和科学,以后我将教大家怎样科学地写一个完善的GL环境。
GLSurfaceView会在一个单独的线程中调用渲染器的方法。既然Android的GLSurfaceView在后台线程中执行渲染,就必须要小心,只能在这个渲染线程中调用OpenGL方法(GL的方法只能在Renderer的三个接口里面使用),在Android的主线程中使用UI相关的调用;两个线程之间的通信可以用如下方法:在主线程中的GLSurfaceView实例可以调用queueEvent()方法传递一个Runnable给后台渲染线程,渲染线程可以调用Activity的runOnUIThread()来传递事件(event)给主线程。
现在基本环境建立起来了,继续我们的曲棍球,先看看下图预览整个实例的展现呗。
一张桌面,一个冰球,两个摇杆。我们就一步步的把这四个对象一一实现。现在先分析桌面。
在桌子被绘制之前,我们需要告诉OpenGL要画什么。开发工程中的第一步是以OpenGL能理解的形式定义一个桌子结构。在OpenGL里,所有东西的结构都是从一个顶点开始。 简单来说,一个顶点就是一个代表几何对象的拐角的点,这个点有很多附加属性(法线向量,色值,自定义参数等);最重要的属性就是位置,它代表了这个顶点在空间中的定位。
我们使用一个长方形代表一桌子,既然一个长方形有4个拐角,我们就需要4个顶点。长方形是一个二维物体,因此每个顶点都需要一个位置,在每个维度上都要有一个坐标。但是遗憾的是,在OpenGL里,只能绘制点,直线,以及三角形。 无论何时,如果我们想表示一个OpenGL中的物体,都要考虑如何用点,直线,三角形把它组合出来。所以 我们的桌子改成如下图这样设计
让我们使用一个数组记录这两个三角形的顶点坐标:
float[] tableVerticesWithTriangles = { // 第一个三角形 0f, 0f, 9f, 14f, 0f, 14f, // 第二个三角形 0f, 0f, 9f, 0f, 9f, 14f };因为一个顶点有两个分量(x,y),所以首先创建一个常量用来记住这一事实:
private static final int POSITION_COMPONENT_COUNT = 2;
好了到了这一步,我们还是一些抽象的概念,我们下一步就要使数据可以被OpenGL存取。在这之前我又先理清一些概念术语:首先OpenGL定义的接口是直接操作硬件相关,接口相关的操作是运行在本地环境上的(Native environment), 但Android的应用程序是不能直接操作本地环境,要使用Java本地接口(JNI),这个其实就是Android系统默认的OpenGL软件开发包提供好了,当调用android.opengl.GLES20包里的方法时,这些接口就是在后台使用JNI调用本地系统库操作硬件了。
入口我们搞清楚了,还有一点就是储存数据的内存分配方式。Java有个 特殊的类集合,他们可以分配本地内存块,并且把Java的数据复制到本地内存。本地内存可以被本地环境存取,而不受垃圾回收器的管控。(如下图所示)
我们继续添加更多的代码,来把顶点数据保存到本地环境。
private static final int BYTES_PER_FLOAT = 4; private final FloatBuffer vertexData; vertexData = ByteBuffer .allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); vertexData.put(tableVerticesWithTriangles);
现在,我们已经定义了桌子的结构,并且把这些数据复制到了本地内存;在把桌子显示到屏幕上之前,我们需要把数据在OpenGL的管道(pipeline)中传递,这就需要使用称为着色器(shader)。这些着色器会告诉图形处理单元(GPU)如何绘制数据。有两种类型的着色器,在绘制任何内容到屏幕之前,需要定义它们俩。
1、顶点着色器(vertex shader)生成每个顶点的最终位置,针对每一个顶点,它都会执行一次;一旦最终位置确定了,OpenGL就可以把这些顶点的集合组装成点、直线以及三角形。
2、片段着色器(fragment shader)为组成点、直线或者三角形的每个片段生成最终的颜色,针对每个片段,它都会执行一次;一个片段是小的、单一颜色的长方形区域,类似计算机屏幕上的一个像素点。
着色器的功能非常强大,很多短视频App的视频效果都是从着色器入手,关于着色器本人也在努力的学习中,这方面我还不敢发布什么文章,学习笔记还在努力整理,希望同道中人共勉。
一旦最后的颜色生成了,OpenGL就会把他们写到一块称为帧缓冲区的内存块中,然后,Android会把这个帧缓冲区显示到屏幕上。下面我们用一张图来整理OpenGL着色器管道的概述。
然后我们再来创建一个顶点着色器和片段着色器,来构建这个OpenGL的管道。 我们在res->raw建立一个新文件命名为:“simple_vertex_shader.glsl”,并添加以下代码:
attribute vec4 a_Position; void main() { gl_Position = a_Position; }这些着色器使用GLSL定义,GLSL是OpenGL的着色语言;这个着色语言的语法结构与C语言相似。更多GLSL的基础信息可以参阅 这里 。对于我们定义过的每个单一的顶点,顶点着色器都会被调用一次;当它被调用的时候,它会在a_Position属性里接收其顶点的位置,这个属性被定义vec4类型。
一个vec4是包含4个分量,在位置的上下文中,可以认为坐标x、y、z和w。 我们仍然把三维顶点视为三元组(x,y,z)。现在引入一个新的分量w,得到向量(x,y,z,w)。请先记住以下两点(稍后我们会给出解释):
● 若w==1,则向量(x, y, z, 1)为空间中的点。
● 若w==0,则向量(x, y, z, 0)为方向向量。
之后,可以定义main(),这是着色器的主要入口点;它所做的就是把前面定义过的位置复制到指定的输出变量gl_Position;这个着色器一定要给gl_Position赋值;OpenGL会把gl_Position中存储的值作为当前顶点的最终位置,并把这些顶点组装点、直线和三角形。
现在已经有了为每个顶点生成组装图元的顶点着色器。我们仍然需要创建一个为每个片段生成最终颜色的片段着色器。片段着色器的主要目的就是告诉GPU每个片段的最终颜色应该是什么。对于点、直线和三角形基本图元的每个片段,片段着色器都会被调用一次,因此如果一个三角形被映射到1000个片段,片段着色器就会被调用1000次。
让我们继续并编写这个片段着色器,我们在res->raw建立一个新文件命名为:“simple_fragment_shader.glsl”
precision mediump float; // 定义数据精度 uniform vec4 u_Color; void main() { gl_FragColor = u_Color; }这个片段着色器的剩余部分与早期定义的顶点着色器一样。不过这次我们要传递一个uniform,它名叫u_Color。如顶点着色器中的位置使用的attribute一样,uniform也是一个四分量向量,在这里分别对应红、绿、蓝和透明值。 接着我们定义了main(),它是这个着色器的主入口点,它把我们在unifrom里定义的颜色复制到那个特殊的输出变量——gl_FragColor。着色器一定要给gl_FragColor赋值,OpenGL会使用这个颜色作为当前片段的最终颜色。
好了,顶点着色器、片段着色器都有了,我们把这些着色器编译并链接在一起,我们就可以把所有的内容放在一起,并告诉OpenGL把曲棍球的桌子画上屏幕显示出来了。
下一节,我们来添加模板代码,实现编译着色器及其屏幕上绘图。
相关文章推荐
- OpenGL.ES在Android上的简单实践:3-曲棍球(顶点归一化、增加颜色)
- OpenGL.ES在Android上的简单实践:4-曲棍球(正交投影解决横屏变形)
- OpenGL.ES在Android上的简单实践:2-曲棍球(编译着色器及屏幕上绘图)
- OpenGL.ES在Android上的简单实践:5-曲棍球(透视投影/模型矩阵)
- OpenGL.ES在Android上的简单实践:9-曲棍球(交互、相交测试)
- OpenGL.ES在Android上的简单实践:8-曲棍球(构建冰球木槌 下 & 模型视图投影矩阵)
- OpenGL.ES在Android上的简单实践:7-曲棍球(构建冰球木槌 上)
- OpenGL.ES在Android上的简单实践:0(前言)
- OpenGL.ES在Android上的简单实践:6-曲棍球(增加纹理,VAO,ShaderProgram)
- android OpenGL(二) 定义顶点和着色器
- Android AIDL(接口定义语言)简单理解和基本使用方法
- OPENGL_ES同样的顶点,可以定义的几何图形可以有所不同
- Unity3d-Android 1s短暂黑屏(原因: 两次Creating OpenGL ES 2.0 context (渲染环境))
- Android JNI(NDK)简单学习(环境搭建,创建一个JNI项目的基本流程)
- OpenGL ES Tutorial for Android – Part I – Setting up the view
- [转] Android Graphic : apk and Skia/OpenGL|ES
- android的基本运行环境
- Android 中的 OpenGL 简单入门(上)
- How to study Android OpenGL ES
- OpenGL ES Tutorial for Android – Part I – Setting up the view