swift技术文章收藏借鉴OpenGL ES图形处理Android

Android OpenGL ES从白痴到入门(二):App诞生

2017-03-13  本文已影响4388人  云华兄

创建工程

首先创建一个Android工程吧,创建工程步骤自己来吧,如果不会,你还是从Android入门开始吧。
添加基本文件(一个包含GLSurfaceView的Activity)



什么?你说我怎么没有androidTest和test这两个包?这两个包好比生孩子时的胎盘,出生就拿去给广东人吃掉了啦。
MainActivity.java

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /* 以下是重点 */
        GLSurfaceView demoGlv = (GLSurfaceView) findViewById(R.id.glv_main_demo);
        // 设置OpenGL版本(一定要设置)
        demoGlv.setEGLContextClientVersion(2); 
        // 设置渲染器(后面会着重讲这个渲染器的类)
        demoGlv.setRenderer(new MyRenderer());
        // 设置渲染模式为连续模式(会以60fps的速度刷新)
        demoGlv.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
        /* 重点结束 */
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.opengl.GLSurfaceView
        android:id="@+id/glv_main_demo"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.icebreaker.opengl">

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <!--决定应用程序最先启动的Activity-->
                <action android:name="android.intent.action.MAIN" />
                <!--决定应用程序是否显示在程序列表里-->
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

接下来就是本文的重点部分了,各位看官要擦亮眼睛准备亮瞎吧!
到此我们已经创建了一个基本的app和OpenGL绘图的基本环境了(GLSurfaceView中的显示缓存Surface,当然也包含了EGL),我们知道SurfaceView实质是将底层显存Surface显示在界面上,而GLSurfaceView做的就是在这个基础上增加OpenGL绘制环境,接下来我们就要开始绘图了。
现在画布有了,我们需要的就是一根画笔和想要画什么样的图形。
在OpenGL中着色器shader就相当于画笔,而顶点vertices相当于图形(我们把一个个点点通过一定的顺序用线连起来是不是就是一个图形呢?)
顶点好理解,就是在画布中各个点所在的位置,这里不过多赘述。
着色器OpenGL中分成两个部分,一个用于绘制顶点的顶点着色器VerticesShader(顶点的位置已经确定好了为何还需要单独一个程序来绘制?在后面你看到摄像机和纹理部分就知道实际传入的顶点坐标并不是简单的对应到屏幕上的坐标,所以顶点着色器的存在有它的 合理性,存在即合理);一个用于给顶点连线后所包围的区域填充颜色的片元着色器FragmentShader,你可以简单的理解成windows中画图的填充工具(对,就是那个油漆桶)


一个不是很贴切的生动例子

说了这么多没代码有什么用?让我们一起来愉快的码代码吧:
MyRenderer.java

public class MyRenderer implements GLSurfaceView.Renderer {
    private int program;
    private int vPosition;
    private int uColor;

    /**
     * 加载制定shader的方法
     * @param shaderType shader的类型  GLES20.GL_VERTEX_SHADER   GLES20.GL_FRAGMENT_SHADER
     * @param sourceCode shader的脚本
     * @return shader索引
     */
    private int loadShader(int shaderType,String sourceCode) {
        // 创建一个新shader
        int shader = GLES20.glCreateShader(shaderType);
        // 若创建成功则加载shader
        if (shader != 0) {
            // 加载shader的源代码
            GLES20.glShaderSource(shader, sourceCode);
            // 编译shader
            GLES20.glCompileShader(shader);
            // 存放编译成功shader数量的数组
            int[] compiled = new int[1];
            // 获取Shader的编译情况
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
            if (compiled[0] == 0) {//若编译失败则显示错误日志并删除此shader
                Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
                Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
                GLES20.glDeleteShader(shader);
                shader = 0;
            }
        }
        return shader;
    }

    /**
     * 创建shader程序的方法
     */
    private int createProgram(String vertexSource, String fragmentSource) {
        //加载顶点着色器
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) {
            return 0;
        }

        // 加载片元着色器
        int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

        // 创建程序
        int program = GLES20.glCreateProgram();
        // 若程序创建成功则向程序中加入顶点着色器与片元着色器
        if (program != 0) {
            // 向程序中加入顶点着色器
            GLES20.glAttachShader(program, vertexShader);
            // 向程序中加入片元着色器
            GLES20.glAttachShader(program, pixelShader);
            // 链接程序
            GLES20.glLinkProgram(program);
            // 存放链接成功program数量的数组
            int[] linkStatus = new int[1];
            // 获取program的链接情况
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
            // 若链接失败则报错并删除程序
            if (linkStatus[0] != GLES20.GL_TRUE) {
                Log.e("ES20_ERROR", "Could not link program: ");
                Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
                GLES20.glDeleteProgram(program);
                program = 0;
            }
        }
        return program;
    }

    /**
     * 获取图形的顶点
     * 特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
     * 转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
     *
     * @return 顶点Buffer
     */
    private FloatBuffer getVertices() {
        float vertices[] = {
                0.0f,   0.5f,
                -0.5f, -0.5f,
                0.5f,  -0.5f,
        };

        // 创建顶点坐标数据缓冲
        // vertices.length*4是因为一个float占四个字节
        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
        vbb.order(ByteOrder.nativeOrder());             //设置字节顺序
        FloatBuffer vertexBuf = vbb.asFloatBuffer();    //转换为Float型缓冲
        vertexBuf.put(vertices);                        //向缓冲区中放入顶点坐标数据
        vertexBuf.position(0);                          //设置缓冲区起始位置

        return vertexBuf;
    }

    /**
     * 当GLSurfaceView中的Surface被创建的时候(界面显示)回调此方法,一般在这里做一些初始化
     * @param gl10 1.0版本的OpenGL对象,这里用于兼容老版本,用处不大
     * @param eglConfig egl的配置信息(GLSurfaceView会自动创建egl,这里可以先忽略)
     */
    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        // 初始化着色器
        // 基于顶点着色器与片元着色器创建程序
        program = createProgram(verticesShader, fragmentShader);
        // 获取着色器中的属性引用id(传入的字符串就是我们着色器脚本中的属性名)
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        uColor = GLES20.glGetUniformLocation(program, "uColor");

        // 设置clear color颜色RGBA(这里仅仅是设置清屏时GLES20.glClear()用的颜色值而不是执行清屏)
        GLES20.glClearColor(1.0f, 0, 0, 1.0f);
    }

    /**
     * 当GLSurfaceView中的Surface被改变的时候回调此方法(一般是大小变化)
     * @param gl10 同onSurfaceCreated()
     * @param width Surface的宽度
     * @param height Surface的高度
     */
    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        // 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
        GLES20.glViewport(0,0,width,height);
    }

    /**
     * 当Surface需要绘制的时候回调此方法
     * 根据GLSurfaceView.setRenderMode()设置的渲染模式不同回调的策略也不同:
     * GLSurfaceView.RENDERMODE_CONTINUOUSLY : 固定一秒回调60次(60fps)
     * GLSurfaceView.RENDERMODE_WHEN_DIRTY   : 当调用GLSurfaceView.requestRender()之后回调一次
     * @param gl10 同onSurfaceCreated()
     */
    @Override
    public void onDrawFrame(GL10 gl10) {
        // 获取图形的顶点坐标
        FloatBuffer vertices = getVertices();

        // 清屏
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

        // 使用某套shader程序
        GLES20.glUseProgram(program);
        // 为画笔指定顶点位置数据(vPosition)
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertices);
        // 允许顶点位置数据数组
        GLES20.glEnableVertexAttribArray(vPosition);
        // 设置属性uColor(颜色 索引,R,G,B,A)
        GLES20.glUniform4f(uColor, 0.0f, 1.0f, 0.0f, 1.0f);
        // 绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);
    }

    // 顶点着色器的脚本
    private static final String verticesShader
            = "attribute vec2 vPosition;            \n" // 顶点位置属性vPosition
            + "void main(){                         \n"
            + "   gl_Position = vec4(vPosition,0,1);\n" // 确定顶点位置
            + "}";

    // 片元着色器的脚本
    private static final String fragmentShader
            = "precision mediump float;         \n" // 声明float类型的精度为中等(精度越高越耗资源)
            + "uniform vec4 uColor;             \n" // uniform的属性uColor
            + "void main(){                     \n"
            + "   gl_FragColor = uColor;        \n" // 给此片元的填充色
            + "}";
}

这里面有几个点稍微提一下,因为由于OpenGL底层是用C/C++实现的所以和java或者其它语音的数据类型字节序列有一定的区别(大小端问题)所以参数传递一般需要做一下转换(我在想,为什么EGL不直接解决呢?真特么偷懒),也就是文中的顶点buffer。代码中我们将顶点着色器和片元着色器的脚本程序编译后链接成一个Program然后我们在绘制图形前就需要设置这么一个Program绘图程序来绘图,Program中的属性(文中的vPosition和uColor)通过获取属性索引值来访问(int类型),不同的数据类型用不同的方法访问(应该是为了方便跨平台移植才没有用重载方式吧)

方法说明

GLES20.glVertexAttribPointer(属性索引,单顶点大小,数据类型,归一化,顶点间偏移量,顶点Buffer)
GLES20.glDrawArrays(绘制方式, 起始偏移, 顶点数量)

源码

点鸡下崽

结束语

以这种代码注释量和我这么一提点,相信以各位看官的聪明才智肯定是可以深刻理解的,这里各位看官不必纠结着色器脚本问题,后续会陆续说明。
后续会引入纹理、变换矩阵、摄像机、着色器脚本程序等等,这将是一个枯燥、痛苦和煎熬的过程,各位看官准备好了么!

什么?你还有些不明白?唯有下图才能解决:


上一篇下一篇

猜你喜欢

热点阅读