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

这篇文章将从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 》一些基本概念

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容