Android OpenGL


首先收藏这个  OpenGL of CSDN

建立OpenGL ES环境

 
为了能在你的Android应用中使用OpenGLES绘画,你必须创建一个view作为容器。而最直接的方式就是从 GLSurfaceViewGLSurfaceView.Renderer分别派生一个类。 GLSurfaceView作为OpenGL绘制所在的容器,而实际的绘图动作都是在 GLSurfaceView.Renderer里面发生的。
使用 GLSurfaceView几乎是整合OpenGLES到你的应用的唯一方式。对于一个全屏或近全屏的graphicsview,它是最好的选择。如果只是在某个小部分显示OpenGLES图形则可以考虑 TextureView。当然如果你比较变态,你完全可以使用 SurfaceView创建一个OpenGLES view。
本教程演示如何完成一个最小实现的OpenGLES2.0应用。
在Manifest中声明使用OpenGLES
 
为了能使用OpenGLES 2.0 API,你必须在你的manifest中添加以下声明:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果你的应用要使用纹理压缩功能,你必须还要声明设备需要支持什么样的压缩格式:
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" /><supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
为OpenGLES Graphics创建一个Activity
 
这个Activity与其它类型应用的Activity并无不同,要说不同,也仅仅是放到Activity的layout的View不一样,你需要放入的是一个 GLSurfaceView
下面的代码演示了使用 GLSurfaceView作为主视图的Acitivity的最少代码实现:
public class OpenGLES20 extends Activity {
            private GLSurfaceView mGLView;

            @Override
            public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);         
                //创建一个GLSurfaceView实例然后设置为activity的ContentView.        
                mGLView = new MyGLSurfaceView(this);
                setContentView(mGLView);
            }
        }

注:OpenGL ES 2.0需要Android2.2 (API Level 8) 及以上版本。

构键一个GLSurfaceView对象

 
GLSurfaceView中其实不需要做太多工作,实际的绘制任务都在 GLSurfaceView.Renderer中了。所以 GLSurfaceView中的代码也非常少,你甚至可以直接使用 GLSurfaceView,但是,然而,别这样做。因为你需要扩展这个类来响应触摸事件啊孩子。
扩展 GLSurfaceView的类像这样写就可以了:

class MyGLSurfaceView extends GLSurfaceView {
            public MyGLSurfaceView(Context context) {
                super(context);         
                //设置Renderer到GLSurfaceView       
                setRenderer(new MyRenderer());
            }
        }


当使用OpenGLES 2.0时,你必须在 GLSurfaceView构造器中调用另外一个函数,它说明了你将要使用2.0版的API:
// 创建一个OpenGL ES 2.0 contextsetEGLContextClientVersion(2);
另一个可以添加的你的 GLSurfaceView实现的可选的操作是设置render模式为只在绘制数据发生改变时才绘制view。使用 GLSurfaceView.RENDERMODE_WHEN_DIRTY
// 只有在绘制数据改变时才绘制viewsetRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
此设置会阻止绘制 GLSurfaceView的帧,直到你调用了 requestRender(),这样会非常高效。
构建一个Renderer类
 
此类控制向 GLSurfaceView的绘制工作。它有三个方法被Android系统调用来计算在 GLSurfaceView上画什么以及如何画。
· 
onSurfaceCreated()- 仅调用一次,用于设置view的OpenGLES环境。
· 
· 
onDrawFrame()- 每次View被重绘时被调用。
· 
· 
onSurfaceChanged()- 如果view的几和形状发生变化了就调用,例如当竖屏变为横屏时。
· 
下面是一个OpenGLES renderer的最基本的实现,它仅在 GLSurfaceView上画了一个灰色的背景:
 public class MyGL20Renderer implements GLSurfaceView.Renderer {
            public void onSurfaceCreated(GL10 unused, EGLConfig config) {
                //设置背景的颜色      
                GLES20.glClearColor(0.5f, 0.5f, 0.5f, 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);
            }
        }

以上就是所有需要做的东西!上面的代码们创建了一个简单的Android应用,它使用OpenGL显示了一个灰色的屏幕。但这段代码并没有做什么有趣的事情,只是为使用OpenGL绘图做好了准备。
注:你可以不明白为什么这些方法们都具有一个 GL10参数,但你使用的却是OpengGLES 2.0 API们。这其实是为了使Android框架能简单的兼容各OpenGLES版本而做的。
如果你对OpenGLES API很熟悉,你其实现在已经可以开始进行绘图了。然而,如果你熟悉,就继续学习下一章吧。
 
 

定义形状

会定义在OpenGLES view上所绘制的形状,是你创建高端图形应用杰作的第一步。如果你不懂OpenGLES定义图形对象的一些基本知识,使用OpenGLES可能有一点棘手。
本文解释OpenGLES相对于Android设备屏幕的坐标系统、定义一个形状的基础知识、形状的外观、以及如何定义三角形和正方形。

定义一个三角形

 
OpenGLEs允许你使用坐本在三个维度上定义绘制对象。所以,在你可以绘制一个三角形之前,你必须定义它的坐标。在OpenGL中,典型的方式是为坐标定义一个浮点类型的顶点数组。为了最高效,你应把这些坐标都写进一个 ByteBuffer,它会被传到OpenGLES图形管线以进行处理。

  class Triangle {
            private FloatBuffer vertexBuffer;
            // 数组中每个顶点的坐标数    
            static final int COORDS_PER_VERTEX = 3;
            static float triangleCoords[] = {
            // 按逆时针方向顺序:         0.0f,  0.622008459f, 0.0f,   
            // top        -0.5f, -0.311004243f, 0.0f,   
            // bottom left         0.5f, -0.311004243f, 0.0f    
            // bottom right    

            // 设置颜色,分别为red, green, blue 和alpha (opacity)    
            float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f};
        } ;

        public Triangle() {
            // 为存放形状的坐标,初始化顶点字节缓冲       
            ByteBuffer bb = ByteBuffer.allocateDirect(
                    // (坐标数 * 4)float占四字节            
                    triangleCoords.length * 4);
            // 设用设备的本点字节序        
            bb.order(ByteOrder.nativeOrder());
            // 从ByteBuffer创建一个浮点缓冲        
            vertexBuffer = bb.asFloatBuffer();
            // 把坐标们加入FloatBuffer中        
            vertexBuffer.put(triangleCoords);
            // 设置buffer,从第一个坐标开始读        
            vertexBuffer.position(0);
        }
        }

缺省情况下,OpenGLES 假定[0,0,0](X,Y,Z) 是 GLSurfaceView 帧的中心,[1,1,0]是右上角,[-1,-1,0]是左下角。
注意此形状的坐标是按逆时针方向定义的。绘制顺序很重要,因为它定义了哪面是形状的正面,哪面是反面,使用OpenGLES 的cullface特性,你可以只画正面而不画反面。

定义一个正方形

 
在OpenGL中定义正方形是十分容易的,有很多方法能做的,但是典型的做法是使用两个三角形
你要为两个三角形都按逆时针方向定义顶点们,并且将这些坐标值们放入一个 ByteBuffer中。为了避免分别为两个三角形定义两个坐标数组,我们使用一个绘制列表来告诉OpenGLES图形管线如果画这些顶点们。下面就是这个形状的代码:

class Square {
            private FloatBuffer vertexBuffer;
            private ShortBuffer drawListBuffer;
            // 每个顶点的坐标数    
            static final int COORDS_PER_VERTEX = 3;
            static float squareCoords[] = {-0.5f, 0.5f, 0.0f,
            // top left                                    -0.5f, -0.5f, 0.0f,   
            // bottom left                                     0.5f, -0.5f, 0.0f,   
            // bottom right                                     0.5f,  0.5f, 0.0f }; 
            // top right   
            private short drawOrder[] = {0, 1, 2, 0, 2, 3};

            // 顶点的绘制顺序 
            public Square() {
                // initialize vertex byte buffer for shape coordinates        
                ByteBuffer bb = ByteBuffer.allocateDirect(
                        // (坐标数 * 4)              
                        squareCoords.length * 4);
                bb.order(ByteOrder.nativeOrder());
                vertexBuffer = bb.asFloatBuffer();
                vertexBuffer.put(squareCoords);
                vertexBuffer.position(0);
                // 为绘制列表初始化字节缓冲       
                ByteBuffer dlb = ByteBuffer.allocateDirect(
                        // (对应顺序的坐标数 * 2)short是2字节            
                        drawOrder.length * 2);
                dlb.order(ByteOrder.nativeOrder());
                drawListBuffer = dlb.asShortBuffer();
                drawListBuffer.put(drawOrder);
                drawListBuffer.position(0);
            }
        }
本例让你见识了用OpenGL如何创建更复杂的形状。通常,你都是使用一群小三(三角形)来绘制对象。下一章,你将学会如何将这些形状画到屏幕上。
 

绘制形状

你定义了要绘制的形状后,你就要画它们了。使用OpenGLES 2.0会形状会有一点点复杂,因为API提供了大量的对渲染管线的控制能力。
本文讲解如何绘制你在前文中定义的那些形状们。

初始化形状

 
在你做任何绘制之前,你必须初始化形状然后加载它。除非形状的结构(指原始的坐标们)在执行过程中发生改变,你都应该在你的Renderer的方法 onSurfaceCreated()中进行内存和效率方面的初始化工作。
public void onSurfaceCreated (GL10 unused, EGLConfig config){
            // 初始化一个三角形    
            mTriangle = new Triangle();
            // 初始化一个正方形  
            mSquare = new Square();
        }

绘制一个形状

 
使用OpenGLES 2.0画一个定义好的形状需要一大坨代码,因为你必须为图形渲染管线提供一大堆信息。典型的,你必须定义以下几个东西:
· 
VertexShader-用于渲染形状的顶点的OpenGLES 图形代码。
· 
· 
FragmentShader-用于渲染形状的外观(颜色或纹理)的OpenGLES 代码。
· 
· 
Program-一个OpenGLES对象,包含了你想要用来绘制一个或多个形状的shader。
· 
你至少需要一个vertexshader来绘制一个形状和一个fragmentshader来为形状上色。这些形状必须被编译然后被添加到一个OpenGLES program中,program之后被用来绘制形状。下面是一个展示如何定义一个可以用来绘制形状的基本shader的例子:
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;" + "}";
        //Shader们包含了OpenGLShading Language (GLSL)代码,必须在使用前编译。要编译这些代码,在你的Renderer类中创建一个工具类方法:
        public static int loadShader ( int type, String shaderCode){
            // 创建一个vertex shader类型(GLES20.GL_VERTEX_SHADER)    
            // 或fragment shader类型(GLES20.GL_FRAGMENT_SHADER)   
            int shader = GLES20.glCreateShader(type);
            // 将源码添加到shader并编译之   
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);
            return shader;
        }

为了绘制你的形状,你必须编译shader代码,添加它们到一个OpenGLES program 对象然后链接这个program。在renderer对象的构造器中做这些事情,从而只需做一次即可。
注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免多次做这些事。如果在运行时你不知道shader的内容,你应该只创建一次code然后缓存它们以避免多次创建。
public Triangle() {
            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
            int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
            mProgram = GLES20.glCreateProgram();
            // 创建一个空的OpenGL ES Program
            GLES20.glAttachShader(mProgram, vertexShader);
            // 将vertex shader添加到program
            GLES20.glAttachShader(mProgram, fragmentShader);
            // 将fragment shader添加到program
            GLES20.glLinkProgram(mProgram);
            // 创建可执行的 OpenGL ES program
        }

此时,你已经准备好增加真正的绘制调用了。需要为渲染管线指定很多参数来告诉它你想画什么以及如何画。因为绘制操作因形状而异,让你的形状类包含自己的绘制逻辑是个很好主意。
创建一个draw()方法负责绘制形状。下面的代码设置位置和颜色值到形状的vertexshader和fragmentshader,然后执行绘制功能。

public void draw() {
        // 将program加入OpenGL ES环境中    
        GLES20.glUseProgram(mProgram);
        // 获取指向vertex shader的成员vPosition的 handle   
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        // 启用一个指向三角形的顶点数组的handle   
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        // 准备三角形的坐标数据    
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);
        // 获取指向fragment shader的成员vColor的handle    
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
        // 设置三角形的颜色   
        GLES20.glUniform4fv(mColorHandle, 1, color, 0);
        // 画三角形    
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
        // 禁用指向三角形的顶点数组    
        GLES20.glDisableVertexAttribArray(mPositionHandle);
    }

一旦你完成这些代码,画这个对象只需要在Renderer的 onDrawFrame()调用draw()方法。当你运行应用时,它应该看起来这样:
图.没有投射和视口时画三角形
此例子中的代码还有很多问题。首先,它不会给人留下印像。其次,三角形会在你从竖屏变为横屏时被压扁。三角形变形的原因是其顶点们没有跟据屏幕的宽高比进行修正。你可以使用投射和视口更正这个问题。那在下一讲了。

应用投影和相机视口

在OpenGLES环境中,投影和相机视口使你绘制的对象以更接近物理对象的样子显示。这是通过对坐标精确的数学变换实现的。
· 
投影-这种变换跟据所在GLSurfaceView的宽和高调整对象的坐标。如果没有此变换,对象会被不规则的视口扭曲。投射变换一般只需要在OpenGLview创建或发生变化时调用,代码写在renderer的onSurfaceChanged()方法中。
· 
· 
相机视口-此变换基于一个虚拟相机的位置调整对象的坐标。注意OpenGLES并没有定义一个真的相机对象,而是提供了一些工具方法变换绘制对象的显示来模拟一个相机。一个相机视口的变换可能只在创建GLSurfaceView时调用,或跟据用户动作动态调用。
· 
本文讲解了如何创建一个投影和一个相机视口然后应用到GLSurfaceView的形状绘制过程。
定义一个投影
 
投影变换的数据是在GLSurfaceView.Renderer 类的 onSurfaceChanged() 方法中计算。下面的例子跟据GLSurfaceView 的宽和高,使用Matrix.frustumM()方法计算出了一个投影变换Matrix:

@Override
        public void onSurfaceChanged (GL10 unused,int width, int height){
            GLES20.glViewport(0, 0, width, height);
            float ratio = (float) width / height;
            // 此投影矩阵在onDrawFrame()中将应用到对象的坐标   
            Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
        }

以下代码产生了一个投影矩阵mProjMatrix ,你可以把它在 onDrawFrame() 方法中与一个相机视口变换结合。
注: 只对你的对象应用一个投影变换一般会导制什么也看不到。通常,你必须也对其应用一个视口变换才能看到东西。


定义一个相机视口

 
再定义一个相机视口变换以使对绘制对象的变换处理变得完整。在下面的例子中,使用方法Matrix.setLookAtM()计算相机视口变换,然后结合前面所计算的投影矩阵。结合后的变换矩阵之后传给要绘制的对象。
 @Override
        public void onDrawFrame (GL10 unused){
            // 设置相机的位置(视口矩阵)    
            Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
            // 计算投影和视口变换    
            Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
            // 绘制形状    
            mTriangle.draw(mMVPMatrix);
        }

应用投影和相机视口变换

 
为了使用前面的合并后的投影和相机视口变换矩阵,修改你的图形对象的方法draw(),接受结果矩阵并应用到你的形状上:
public void draw(float[] mvpMatrix) {
        // 传递计算出的变换矩阵    ...            
        // 获得形状的变换矩阵的handle            
        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        // 应用投影和视口变换         
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
        // 绘制三角形         
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
    }



一旦你正确的计算和应用了投影和视口变换,你的图像将出现在正确的位置,看起来像下面这样:
图.应用了投影和视口变换后绘制的三角形
现在你拥有了一个正确显示你的形状的应用了,是让你的图形动起来的时候了...嘿嘿...
 

添加运动

在屏幕上绘制是OpenGL的基础能力,但是你也可以用其它的Android图形框架类来做,包括Canvas和Drawable。 但是OpenGL ES提供了另外的能力,可以在三维上移动和变换对象。总之它能创造很牛B的用户体验。在本文中,你将学会如何使用OpenGL ES为形状添加旋转功能。
转动一个形状
使用OpenGL ES 2.0旋转一个对象也是十分简单地。你创建另外一个变换矩阵(一个旋转矩阵)然后把它合并到你的投影和相机视口变换矩阵就行了:

private float[] mRotationMatrix = new float[16];
        public void onDrawFrame (GL10 gl){
            ...
            // 为三角形创建一个旋转变换  
            long time = SystemClock.uptimeMillis() % 4000L;
            float angle = 0.090f * ((int) time);
            Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

            // 把旋转矩阵合并到投影和相机矩阵  
            Matrix.multiplyMM(mMVPMatrix, 0, mRotationMatrix, 0, mMVPMatrix, 0);

            // 画三角形  
            mTriangle.draw(mMVPMatrix);
        }

如果你的三角形在此新后转不起来,则要查看是否把GLSurfaceView.RENDERMODE_WHEN_DIRTY 设置注释了,下面马上就讲到。
启用持续渲染
到现在,你应在代码中注释掉设置只在数据改变时才渲染的代码,否则,OpenGL 只有转一次然后等待直到GLSurfaceView 的包含者调用requestRender():
public MyGLSurfaceView(Context context) {
            ...
            // Render the view only when there is a change in the drawing data  
            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 
            // 注释掉以自动旋转  
        }

除非你不让对象与用户有交互,否则启用这个设置是一个好做法。要准备解除这句的注释了,因为下一讲会用到它。

响应触摸事件

使你的OpenGL ES应用能响应触摸的关键是扩展你实现的GLSurfaceView 代码,覆写onTouchEvent() 方法来监听触摸事件。 
本文向你展示如何监听用户的触摸事件以使用户可以旋转某个OpenGL ES对象。
设置一个触摸监听器
为了使你的OpenGL Es应用响应触摸事件,你必须在你的GLSurfaceView 类中实现onTouchEvent()事件。下面的例子演示了如何监听MotionEvent.ACTION_MOVE 事件然后把它们转换成一个形状的旋转角度。

@Override
        public boolean onTouchEvent (MotionEvent e){
            // MotionEvent带有从触摸屏幕来的输入的详细信息以及其它输入控制  
            // 此处,你只需对触摸位置的改变感兴趣即可。  

            float x = e.getX();
            float y = e.getY();

            switch (e.getAction()) {
                case MotionEvent.ACTION_MOVE:

                    float dx = x - mPreviousX;
                    float dy = y - mPreviousY;

                    // reverse direction of rotation above the mid-line  
                    if (y > getHeight() / 2) {
                        dx = dx * -1;
                    }

                    // reverse direction of rotation to left of the mid-line  
                    if (x < getWidth() / 2) {
                        dy = dy * -1;
                    }

                    mRenderer.mAngle += (dx + dy) * TOUCH_SCALE_FACTOR;  // = 180.0f / 320  
                    requestRender();
            }

            mPreviousX = x;
            mPreviousY = y;
            return true;
        }

注意在计算完旋转角度之后,本方法调用requestRender() 来告诉renderer要渲染帧了。这样做是很高效的,因为在没有发生旋转时不需要重画帧。然而,在你没有要求只在数据发生改变才重画之前,还不能达到最高效,即别忘了解除这一句的注释:

public MyGLSurfaceView(Context context) {
            ...
            // Render the view only when there is a change in the drawing data  
            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        }


曝露出旋转角度

上面的例子要求你向其它类曝露出你的旋转角度,所以你要为你的renderer添加一个public成员。既然renderer的代码运行于主界面之外的单独线程中,你必须声明这个公开变量为volatile. 。下面的代码就是这样做的:
public class MyGLRenderer implements GLSurfaceView.Renderer {

            public volatile float mAngle;
        }

应用旋转

要应用触摸所产生的旋转, 注释掉产生角度的代码并且添加mAngle,它包活了触摸所产生的角度:
public void onDrawFrame (GL10 gl){

            // Create a rotation for the triangle  
            // long time = SystemClock.uptimeMillis() % 4000L;  
            // float angle = 0.090f * ((int) time);  
            Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

            // 合并旋转矩阵到投影和相机视口矩阵  
            Matrix.multiplyMM(mMVPMatrix, 0, mRotationMatrix, 0, mMVPMatrix, 0);

            // 画一个角度  
            mTriangle.draw(mMVPMatrix);
        }


当你完成了上述的几步,运行程序然后在陪同幕上拖动你的手指头,你会看到下面这样:
 Figure . 跟据触摸输入的转动的三角形(圈圈显示了触摸的位置)。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值