美文网首页SurfaceViewUnityShaderH5游戏开发
《OpenGL从入门到放弃02 》GLSurfaceView和R

《OpenGL从入门到放弃02 》GLSurfaceView和R

作者: 蓝师傅_Android | 来源:发表于2019-04-14 22:48 被阅读0次

    这篇文章将从demo开始介绍 GLSurfaceView 和 Renderer的使用。
    如果对OpenGL的一些基本概念不清楚可以第一篇文章
    《OpenGL从入门到放弃01 》一些基本概念

    1、GLSurfaceView

    GlSurfaceView继承自SurfaceView。并增加了Renderer接口,提供三个回调方法

    先看下一般使用方法

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            GLSurfaceView glSurfaceView = new GLSurfaceView(this);
            glSurfaceView.setRenderer(new GLSurfaceView.Renderer() {
                @Override
                public void onSurfaceCreated(GL10 gl, EGLConfig config) {
                    
                }
    
                @Override
                public void onSurfaceChanged(GL10 gl, int width, int height) {
    
                }
    
                @Override
                public void onDrawFrame(GL10 gl) {
    
                }
            });
            setContentView(glSurfaceView);
        }
    
    1. 创建 GLSurfaceView
    2. 调用glSurfaceView.setRenderer,为GLSurfaceView设置一个Renderer,并重写三个方法

    2、GlSurfaceView.Renderer

    GlSurfaceView.Renderer 提供和三个渲染回调方法

    public interface Renderer {
        void onSurfaceCreated(GL10 gl, EGLConfig config);
        void onSurfaceChanged(GL10 gl, int width, int height);
        void onDrawFrame(GL10 gl);
    }
    
    • onSurfaceCreated: GlSurfaceView 创建的时候回调,可以做一些参数初始化操作
    • onSurfaceChanged:GlSurfaceView尺寸发送变化时回调,例如横竖屏切换
    • onDrawFrame:此方法频繁回调,我们可以在这个方法里面进行绘制操作

    怎么知道 onDrawFrame 会频繁回调?来,上源码


    GLSurfaceView 是一个View对象,在onAttachedToWindow方法启动一个渲染线程

    protected void onAttachedToWindow() {
            super.onAttachedToWindow();
            ...
            mGLThread = new GLThread(mThisWeakRef);
                if (renderMode != RENDERMODE_CONTINUOUSLY) {
                    mGLThread.setRenderMode(renderMode);
                }
                mGLThread.start();
        }
    

    GLThread 继承自Thread,run方法里调用了guardedRun 方法,重点来了


    private void guardedRun() throws InterruptedException {
           ...
           while (true) {
                // 1 onSurfaceCreated 只会调用一次,调用之后createEglContext就为false了
                if (createEglContext) {
                            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
                            if (view != null) {
                                try {
                                    view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
                                }
                            }
                            // 赋值为false,说明onSurfaceCreated只执行一次
                            createEglContext = false;
                        }
                        ...
                 // 2 大小改变的时候调用onSurfaceChanged
                if (sizeChanged) {
                            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
                            if (view != null) {
                                try {
                                    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceChanged");
                                    view.mRenderer.onSurfaceChanged(gl, w, h);
                                }
                            }
                            sizeChanged = false;
                        }
                        ...
                 // 3 每次都调用  onDrawFrame  
                {
                            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
                            if (view != null) {
                                try { view.mRenderer.onDrawFrame(gl);
                                } 
                            }
                        }
                        
             }   
        
            
           
    }
    

    从注释1、2、3处我们可以验证 Renderer接口 三个方法的调用时机。


    上面这些貌似理解起来没啥问题,但是绘制图形就复杂一点了。

    3、先来简单的,画一个背景

    3.1 声明OpenGL版本

    在使用OpenGL之前,需要在AndroidManifest.xml中设置OpenGL的版本:这里我们使用的是OpenGl ES 2.0,所以需要添加如下说明:
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />

    3.2 GLSufaceView 准备

    在Activity onCreate中创建 GLSufaceView 和设置Renderer。GLSufaceView可以写在xml中,一样的。

    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            GLSurfaceView glSurfaceView = new GLSurfaceView(this);
            glSurfaceView.setRenderer(new DemoRenderer());
            setContentView(glSurfaceView);
        }
    

    DemoRenderer 实现了GLSurfaceView.Renderer接口,并且重写三个方法,继续看

    3.3 GlSurfaceView.Renderer中的绘制步骤

    • 设置展示窗口(viewport):GLES20.glViewport(0,0,width,height);
    • 创建图形类,确定好顶点位置和图形颜色,将顶点和颜色数据转换为OpenGl使用的数据格式
    • 加载顶点着色器和片元着色器用来修改图形的颜色,纹理,坐标等属性
    • 创建投影和相机视图来显示视图的显示状态,并将投影和相机视图的转换传递给着色器。
    • 创建项目(Program),连接顶点着色器片段着色器
    • 将坐标数据传入到OpenGl ES程序中

    绘制步骤大概是这些,接下来上代码了。

    3.4 画个背景色看看效果

    public class DemoRenderer implements GLSurfaceView.Renderer {
        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            // 设置个红色背景
            GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
        }
    
        public void onDrawFrame(GL10 unused) {
            // Redraw background color 重绘背景
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        }
    
        public void onSurfaceChanged(GL10 unused, int width, int height) {
            // 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
            GLES20.glViewport(100,100,width,height);
        }
    }
    
    
    image.png

    很简单,就画一个背景色而已。

    4、画一个三角形

    创建一个几何图形(这里列举三角形),需要注意一点,我们设置图形的顶点坐标后,需要将顶点坐标转为ByteBuffer,这样OpenGL才能进行图形处理

    4.1 定义一个三角形View

    网上很多demo画三角形都是在Renderer里面,这里我们将三角形的绘制流程抽取到一个单独的类,定义为 GLTriangle,在构造方法里面初始化数据,然后定义一个draw方法,在onDrawFrame()中调用draw方法进行绘制操作。

    public class GLTriangle{
    
        // 顶点着色器的脚本
        String vertexShaderCode =
                " attribute vec4 vPosition;" +     // 应用程序传入顶点着色器的顶点位置
                        " void main() {" +
                        "     gl_Position = vPosition;" +  // 此次绘制此顶点位置
                        " }";
    
        // 片元着色器的脚本
        String fragmentShaderCode =
                " precision mediump float;" +  // 设置工作精度
                        " uniform vec4 vColor;" +       // 接收从顶点着色器过来的顶点颜色数据
                        " void main() {" +
                        "     gl_FragColor = vColor;" +  // 给此片元的填充色
                        " }";
    
        private FloatBuffer vertexBuffer;  //顶点坐标数据要转化成FloatBuffer格式
    
        // 数组中每3个值作为一个坐标点
        static final int COORDS_PER_VERTEX = 3;
        //三角形的坐标数组
        static float triangleCoords[] = {
                0.0f, 0.5f, 0.0f, // top
                -0.5f, -0.5f, 0.0f, // bottom left
                0.5f, -0.5f, 0.0f  // bottom right
        };
    
        //顶点个数,计算得出
        private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
        //一个顶点有3个float,一个float是4个字节,所以一个顶点要12字节
        private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
    
        //三角形的颜色数组,rgba
        private float[] mColor = {
                0.0f, 1.0f, 0.0f, 1.0f,
        };
    
        //当前绘制的顶点位置句柄
        private int vPosition;
        //片元着色器颜色句柄
        private int vColor;
        //这个可以理解为一个OpenGL程序句柄
        private final int mProgram;
    
    
        public GLTriangle() {
            /** 1、数据转换,顶点坐标数据float类型转换成OpenGL格式FloatBuffer,int和short同理*/
            vertexBuffer = GLUtil.floatArray2FloatBuffer(triangleCoords);
    
            /** 2、加载编译顶点着色器和片元着色器*/
            int vertexShader = GLUtil.loadShader(GLES20.GL_VERTEX_SHADER,
                    vertexShaderCode);
            int fragmentShader = GLUtil.loadShader(GLES20.GL_FRAGMENT_SHADER,
                    fragmentShaderCode);
    
            /** 3、创建空的OpenGL ES程序,并把着色器添加进去*/
            mProgram = GLES20.glCreateProgram();
    
            // 添加顶点着色器到程序中
            GLES20.glAttachShader(mProgram, vertexShader);
    
            // 添加片段着色器到程序中
            GLES20.glAttachShader(mProgram, fragmentShader);
    
            /** 4、链接程序*/
            GLES20.glLinkProgram(mProgram);
    
        }
    
    
       
        public void draw() {
    
            // 将程序添加到OpenGL ES环境
            GLES20.glUseProgram(mProgram);
    
            /***在什么位置显示什么颜色*/
    
            // 获取顶点着色器的位置的句柄(这里可以理解为当前绘制的顶点位置)
            vPosition = GLES20.glGetAttribLocation(mProgram, "vPosition");
    
            // 启用顶点属性
            GLES20.glEnableVertexAttribArray(vPosition);
    
            //准备三角形坐标数据
            GLES20.glVertexAttribPointer(vPosition, COORDS_PER_VERTEX,
                    GLES20.GL_FLOAT, false,
                    vertexStride, vertexBuffer);
    
            // 获取片段着色器的vColor属性
            vColor = GLES20.glGetUniformLocation(mProgram, "vColor");
    
            // 设置绘制三角形的颜色
            GLES20.glUniform4fv(vColor, 1, mColor, 0);
    
            // 绘制三角形
            GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
    
            // 禁用顶点数组
            GLES20.glDisableVertexAttribArray(vPosition);
        }
    }
    
    

    代码基本都加了注释,在构造函数中主要做的事是:

    1. 顶点数据格式转换,转成成OpenGL能识别的数据格式

    为什么数据需要转换格式呢?主要是因为Java的缓冲区数据存储结构为大端字节序(BigEdian),而OpenGl的数据为小端字节序(LittleEdian),因为数据存储结构的差异,所以,在Android中使用OpenGl的时候必须要进行下转换。

    /**
         * float 数组转换成FloatBuffer,OpenGL才能使用
         * @param arr
         * @return
         */
        public static FloatBuffer floatArray2FloatBuffer(float[] arr)
        {
            FloatBuffer mBuffer;
            // 初始化ByteBuffer,长度为arr数组的长度*4,因为一个int占4个字节
            ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4);
            // 数组排列用nativeOrder
            qbb.order(ByteOrder.nativeOrder());
            mBuffer = qbb.asFloatBuffer();
            mBuffer.put(arr);
            mBuffer.position(0);
            return mBuffer;
        }
    
    1. 加载和编译定义好的顶点着色器和片元着色器代码

    这里面有两个知识点,一个是着色器语言,一个是编译过程。
    对于着色器代码,加了注释,大概意思能看懂就行,后面会写一篇专门讲解着色器语言。

    着色器语言需要经过加载和编译之后,链接到OpenGL ES程序中

    public static int loadShader(int shaderType, String source) {
            // 创造顶点着色器类型(GLES20.GL_VERTEX_SHADER)
            // 或者是片段着色器类型 (GLES20.GL_FRAGMENT_SHADER)
            int shader = GLES20.glCreateShader(shaderType);
            // 添加上面编写的着色器代码并编译它
            GLES20.glShaderSource(shader, source);
            GLES20.glCompileShader(shader);
            return shader;
        }
    

    加载和编译,这些都是固定的步骤

    1. 创建空的 OpenGL ES程序,并把着色器句柄添加进去(着色器句柄可以理解为这个着色器的id)
    2. 链接程序

    初始化OpenGL ES程序4个步骤基本是固定的,为OpenGL绘制做准备


    接下来看下draw方法:

    1. 构造方法中已经把程序(mProgram)准备好了,还需要将程序添加到OpenGL ES环境:GLES20.glUseProgram(mProgram);
    2. 准备三角形的坐标数据
    // 获取顶点着色器的位置的句柄(这里可以理解为当前绘制的顶点位置)
    vPosition = GLES20.glGetAttribLocation(mProgram, "vPosition");
    
    // 启用顶点属性
    GLES20.glEnableVertexAttribArray(vPosition);
    
    //准备三角形坐标数据(这里可以理解为将数据传到顶点着色器的vPosition变量)
    GLES20.glVertexAttribPointer(vPosition, COORDS_PER_VERTEX,
            GLES20.GL_FLOAT, false,
            vertexStride, vertexBuffer);
    
    1. 设置绘制三角形的颜色
    // 获取片段着色器的vColor句柄
    vColor = GLES20.glGetUniformLocation(mProgram, "vColor");
    
    // 设置绘制三角形的颜色
    GLES20.glUniform4fv(vColor, 1, mColor, 0);
    
    1. 绘制三角形 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    对于三角形的封装,代码不多,步骤也还算清晰,那怎么使用应该能猜到吧

    public class DemoRenderer implements GLSurfaceView.Renderer {
    
        private GLTriangle mGlTriangle;
        
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            mGlTriangle = new GLTriangle();
        }
        
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            // 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
            GLES20.glViewport(100,100,width,height);
        }
    
        @Override
        public void onDrawFrame(GL10 gl) {
            mGlTriangle.draw();
        }
    
    }
    
    image.png

    图片偏右,这是因为GLES20.glViewport(100,100,width,height);,xy值不为0,

    至此,一个简单的三角形就绘制好了,
    对于习惯使用Android 原生控件的看官来说,OpenGL可能是完全陌生的,需要时间慢慢消化才行,这一节的内容也就到此为止。


    另外,大家有没有发现这个三角形形状有点怪怪的,坐标是

    static float triangleCoords[] = {
                0.0f, 0.5f, 0.0f, // top
                -0.5f, -0.5f, 0.0f, // bottom left
                0.5f, -0.5f, 0.0f  // bottom right
        };
    

    我们要的是等边的,为什么会显示成这样呢?
    第一节介绍概念时有说到OpenGL的坐标系,没错,就是因为坐标问题啦,下一节将介绍投影和相机视图来解决这个问题。

    上一篇:《OpenGL从入门到放弃01 》一些基本概念

    相关文章

      网友评论

        本文标题:《OpenGL从入门到放弃02 》GLSurfaceView和R

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