OpenGL.ES在Android上的简单实践:1-曲棍球(基本环境和定义顶点)

 

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把曲棍球的桌子画上屏幕显示出来了。

 

        下一节,我们来添加模板代码,实现编译着色器及其屏幕上绘图。

 

 

 

 

 

 

 

 

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值