您的位置:首页 > 移动开发 > Android开发

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把曲棍球的桌子画上屏幕显示出来了。
        下一节,我们来添加模板代码,实现编译着色器及其屏幕上绘图。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: