美文网首页swift技术文章收藏借鉴OpenGL ES图形处理Android
Android OpenGL ES从白痴到入门(二):App诞生

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

作者: 云华兄 | 来源:发表于2017-03-13 11:10 被阅读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(绘制方式, 起始偏移, 顶点数量)

源码

点鸡下崽

结束语

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

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


相关文章

网友评论

  • fded4aaa6b11:迷迷糊糊到了新公司,做这个,完全没接触过,期间靠“毅力”改了一些bug。看了你的文章,感觉有一定认识,继续看,看完再看~!很棒,写的非常简单明了。真的不是那些自以为自己牛逼,不把话说清楚的人能比的~!:+1: 超棒
    云华兄:@diguagege 没人带是要花比较多的时间
    fded4aaa6b11:@云华兄 兄弟,你说的极其痛苦都是简单的了,没有人帮忙,自己摸,你能摸出来都是花了非常多的时间了。我今天就一下午看了你的文章,再来看项目代码,好几个问题,我都知道根源了。超棒~!
    云华兄:@diguagege 能帮到你是我最大的荣幸,我开始学也是极其痛苦,所以才写的这个系列。opengl越来越多人用,相信会有很多人跟我一样,一条曲折的路走一次就够了😏
  • b06a4bb669ec:三天了,全网搜到最舒服的教程
    云华兄:@西贝一了 希望能对你有所帮助😄
  • 过期的薯条:想问一个问题,设置的顶点是怎么设置的,随便写的 就不怕显示不出来吗
    云华兄:@过期的薯条 肯定不是随便写的啊,要画什么样的图形就给什么样的顶点
  • d9dc2aa471c4:关于立体图形的编辑,不打算搞个教程吗?你的教程我看的很舒服:smile:
    云华兄:@Escape永恒 看缘分吧,这个都一直没时间继续写下去😂
  • 放纵的卡尔:vertices[] = {
    0.0f, 0.5f,
    -0.5f, -0.5f,
    0.5f, -0.5f,
    }
    这三个点怎么确定的位置?没看懂.谁能解答下.
    云华兄:@放纵的卡尔 特效用属性动画或者自定义view就可以了
    放纵的卡尔:@云华兄 大神这么晚还回复. 我找了篇文章研究了下坐标系,基本搞懂了. 我想着有没有种简单点的游戏引擎,可以在APP中开发点特效啥的,这么整OpenGL好崩溃:smile:
    云华兄:@放纵的卡尔 这三个点就是要绘制的三角形的三个顶点(显示区坐标取值范围在-1~1之间),在绘制的时候传给顶点着色器,在顶点着色器中可以再进行变换(这里并没有再做变换),最后使用GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);绘制,GLES20.GL_TRIANGLE_STRIP表示绘制的三角形的方式(三角形绘制有好几种方式)
  • 5d111b3274bb:大佬能给下微信或者qq吗,方便膜拜
    云华兄:@5d111b3274bb 怎么都问我这个?我微信号就在资料里面,有一个二维码
  • 5d111b3274bb:作者写的太好了,牛逼牛逼
  • c9d81907e87e:拜读了,非常有帮助
  • 小白学AI:感谢,完全明白了
  • 夏侯撒:谢谢 非常有帮助
  • Benhero:这是见过的注释最完整的了,看了之后直接可以从痴汉到入门,666
    期待更新!:+1:
    云华兄:能帮到你这篇文章也就值了,有空的话会继续更新

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

本文链接:https://www.haomeiwen.com/subject/hpcjgttx.html