《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 会频繁回调?来,上源码


《OpenGL从入门到放弃02 》GLSurfaceView和Renderer_第1张图片

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 方法,重点来了


《OpenGL从入门到放弃02 》GLSurfaceView和Renderer_第2张图片
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,所以需要添加如下说明:

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);
    }
}

《OpenGL从入门到放弃02 》GLSurfaceView和Renderer_第3张图片
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和Renderer)