OpenGL ES——一个平平无奇的三角形

2018-07-03 13:27:44 浏览数 (1)

前言

随着VR/AR技术的普及,人机交互的模式将产生新的变革。OpenGL ES作为移动端上的图像渲染框架,将变得越来越重要。在此将学习OpenGL ES作为Q3的主要目标。在10月1日前,希望能有阶段性成果。

快速开始

判断设备是否支持OpenGL ES

代码语言:javascript复制
    fun checkSupported() : Boolean{
        var supportsEs2 = false;
        val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
        val configurationInfo = activityManager.getDeviceConfigurationInfo();
        supportsEs2 = configurationInfo.reqGlEsVersion >= 0x2000;

        val isEmulator = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
                && (Build.FINGERPRINT.startsWith("generic")
                || Build.FINGERPRINT.startsWith("unknown")
                || Build.MODEL.contains("google_sdk")
                || Build.MODEL.contains("Emulator")
                || Build.MODEL.contains("Android SDK built for x86"));

        supportsEs2 = supportsEs2 || isEmulator

        return supportsEs2

    }

生命周期

代码语言:javascript复制
    override fun onPause() {
        super.onPause()
        glSurfaceView.let { glSurfaceView.onPause() }
    }

    override fun onResume() {
        super.onResume()
        glSurfaceView.let { glSurfaceView.onResume() }
    }

用OpenGL渲染Activity

代码语言:javascript复制
    lateinit var glSurfaceView: GLSurfaceView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (checkSupported()) {
            glSurfaceView = GLSurfaceView(this);
            glSurfaceView.let { glSurfaceView.setRenderer(GLRender2())
                setContentView(glSurfaceView); }
        } else {
            Toast.makeText(this, "当前设备不支持OpenGL ES 2.0!", Toast.LENGTH_SHORT).show();
        }

    }

我们可以看到,OpenGL实际的渲染逻辑,全部封装在了我自己创建的GLRender2中。

以上代码,就是OpenGL渲染Activity最简单的外部框架。

渲染逻辑

代码语言:javascript复制
public class GLRender2 implements GLSurfaceView.Renderer {


    private float[] mTriangleArray = {
            0f, 1f, 0f,
            -1f, -1f, 0f,
            1f, -1f, 0f
    };
    //三角形各顶点颜色(三个顶点)
    private float[] mColor = new float[]{
            1, 1, 0, 1,
            0, 1, 1, 1,
            1, 0, 1, 1
    };
    private FloatBuffer mTriangleBuffer;
    private FloatBuffer mColorBuffer;


    public GLRender2() {
        Log.d("GLRender2" , "call GLRender init");
        //点相关
        //先初始化buffer,数组的长度*4,因为一个float占4个字节
        ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
        //以本机字节顺序来修改此缓冲区的字节顺序
        bb.order(ByteOrder.nativeOrder());
        mTriangleBuffer = bb.asFloatBuffer();
        //将给定float[]数据从当前位置开始,依次写入此缓冲区
        mTriangleBuffer.put(mTriangleArray);
        //设置此缓冲区的位置。如果标记已定义并且大于新的位置,则要丢弃该标记。
        mTriangleBuffer.position(0);


        //颜色相关
        ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
        bb2.order(ByteOrder.nativeOrder());
        mColorBuffer = bb2.asFloatBuffer();
        mColorBuffer.put(mColor);
        mColorBuffer.position(0);
    }

    @Override
    public void onDrawFrame(GL10 gl) {

        Log.d("GLRender2" , "call onDrawFrame");
        // 清除屏幕和深度缓存
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        // 重置当前的模型观察矩阵
        gl.glLoadIdentity();

        // 允许设置顶点
        //GL10.GL_VERTEX_ARRAY顶点数组
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 允许设置颜色
        //GL10.GL_COLOR_ARRAY颜色数组
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        //将三角形在z轴上移动
        gl.glTranslatef(0f, 0.0f, -2.0f);

        // 设置三角形
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
        // 设置三角形颜色
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
        // 绘制三角形
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);


        // 取消颜色设置
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
        // 取消顶点设置
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

        //绘制结束
        gl.glFinish();

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.d("GLRender2" , "call onSurfaceChanged");
        float ratio = (float) width / height;
        // 设置OpenGL场景的大小,(0,0)表示窗口内部视口的左下角,(w,h)指定了视口的大小
        gl.glViewport(0, 0, width, height);
        // 设置投影矩阵
        gl.glMatrixMode(GL10.GL_PROJECTION);
        // 重置投影矩阵
        gl.glLoadIdentity();
        // 设置视口的大小
        gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
        //以下两句声明,以后所有的变换都是针对模型(即我们绘制的图形)
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.d("GLRender2" , "call onSurfaceCreated");
        // 设置白色为清屏
        gl.glClearColor(1, 1, 1, 1);

    }

}

以上代码,渲染出一个变色的三角形:

详细介绍

GLRender2

这个平平无奇的三角形,它的渲染逻辑究竟是什么样的呢? 在此之前,我们需要先了解GLRender2是一个怎样的类。

GLRender2实现了GLSurfaceView.Renderer接口。需要实现三个方法:

代码语言:javascript复制
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.d("GLRender" , "call onSurfaceCreated");
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.d("GLRender" , "call onSurfaceChanged");
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        Log.d("GLRender" , "call onDrawFrame");
    }

生命周期

这是GLSurfaceView生命周期的三个环节。

onSurfaceCreated

onSurfaceCreatedGLRender2被初始化后首先调用。通常用于初始化伴随GLSurfaceView整个生命周期的数据和设置初始颜色。

onSurfaceChanged

onSurfaceChanged 当GLSurfaceView大小改变时,对应的Surface大小也会改变。值得注意的是,在Surface刚创建的时候,它的size其实是0,也就是说在画第一次图之前它也会被调用一次的。(而且对于很多时候,Surface的大小是不会改变的,那么此函数就只在创建之初被调用一次而已)

原型如下:

代码语言:javascript复制
public abstract void onSurfaceChanged (GL10 gl, int width, int height)

另外值得注意的是,它告诉了我们这张纸有多高多宽。这点很重要。因为在onSurfaceCreated的时候我们是不知道纸的宽高的,所以有一些和长宽相关的初始化工作还得在此函数中来做。

onDrawFrame

以后会有两种模式供你选择:

  • RENDERMODE_CONTINUOUSLY
  • RENDERMODE_WHEN_DIRTY

第一种模式(RENDERMODE_CONTINUOUSLY): 连续不断的刷,画完一幅图马上又画下一幅。这种模式很明显是用来画动画的;

第二种模式(RENDERMODE_WHEN_DIRTY): 只有在需要重画的时候才画下一幅。这种模式就比较节约CPU和GPU一些,适合用来画不经常需要刷新的情况。多说一句,系统如何知道需要重画了呢?当然是你要告诉它…… 调用GLSurfaceView的requestRender ()方法,使其重绘。

GLSurfaceView的setRenderMode(int renderMode)方法。可以供你设置你需要的刷新模式。

设置背景色

代码语言:javascript复制
// 设置白色为清屏
gl.glClearColor(1, 1, 1, 1);

设置场景大小

代码语言:javascript复制
 // 设置OpenGL场景的大小,(0,0)表示窗口内部视口的左下角,(w,h)指定了视口的大小
gl.glViewport(0, 0, width, height);

设置投影矩阵

在渲染中,我们只绘制可见的东西。所以我们需要将真实物体转化到可见区域,即谓之投影矩阵。

代码语言:javascript复制
// 设置投影矩阵
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩阵
gl.glLoadIdentity();
// 设置视口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);

这三句将真实物体映射到坐标系中。这个地方可能有点难以理解。

虽然在OpenGL中,我们画的是3D物体,但手机屏幕毕竟是一个平面。我们在生活中,看见的也只是一个平面。那么,一个3D物体,我们看到的应该是什么样的,取决于我们的投影矩阵如何设置。

假设,我们的三角形,三个点分别是:

代码语言:javascript复制
    private float[] mTriangleArray = {
            0f, 1f, -2f,
            -1f, -1f, -2f,
            1f, -1f, -2f
    };

那么,这个三角形其实是在z轴为-2处的一个平面。我们用下图的方式,进行观察。

下图,近处的平面,距离视点为1,远处的为10。我们画的三角平面,就在距离视点2的位置。在距离视点1处,我们的视口大小是 2ratio x 2。到距离2处,我们的视口大小一定为2ratio x 2。

所以此时,我们渲染我们的三角形,它的高一定为画布高度的1/2。

如果我们将近平面,视点距离改为0.5f。同样的三角形,我们渲染出来高度一定为画布高度的1/4。

如果我们将三角形改为:

代码语言:javascript复制
    private float[] mTriangleArray = {
            0f, 1f, -1f,
            -1f, -1f, -2f,
            1f, -1f, -2f
    };

视点距离改为1.0f 。三角形的高度将变为画布高度的3/4。

而远平面的视点距离,则决定了我们可以看到多远的元素。比如我们还是三角形为:

代码语言:javascript复制
    private float[] mTriangleArray = {
            0f, 1f, -1f,
            -1f, -1f, -2f,
            1f, -1f, -2f
    };

将远平面视点距离改为1.5f,此时我们将只能看到上个例子中三角形的上半部分。

其中变化读者可以画立体图,慢慢感受一下。

回归模型

完成了对投影的操作后,我们将操作模式设置到模型操作。

代码语言:javascript复制
        //以下两句声明,以后所有的变换都是针对模型(即我们绘制的图形)
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

完成了种种矩阵的设置后,我们可以开始进行绘制了。

图形和色彩数据

OpenGL并不是对堆里面的数据进行操作,而是在直接内存中(Direct Memory),即操作的数据需要保存到NIO里面的Buffer对象中。而我们上面声明的float[]对象保存在堆中,因此,需要我们将float[]对象转为java.nio.Buffer对象。

代码语言:javascript复制
    private float[] mTriangleArray = {
            0f, 1f, 1f,
            -1f, -1f, 0f,
            1f, -1f, 0f
    };
    //三角形各顶点颜色(三个顶点)
    private float[] mColor = new float[]{
            1, 1, 0, 1,
            0, 1, 1, 1,
            1, 0, 1, 1
    };
    private FloatBuffer mTriangleBuffer;
    private FloatBuffer mColorBuffer;


    public GLRender2() {
        Log.d("GLRender2" , "call GLRender init");
        //点相关
        //先初始化buffer,数组的长度*4,因为一个float占4个字节
        ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
        //以本机字节顺序来修改此缓冲区的字节顺序
        bb.order(ByteOrder.nativeOrder());
        mTriangleBuffer = bb.asFloatBuffer();
        //将给定float[]数据从当前位置开始,依次写入此缓冲区
        mTriangleBuffer.put(mTriangleArray);
        //设置此缓冲区的位置。如果标记已定义并且大于新的位置,则要丢弃该标记。
        mTriangleBuffer.position(0);


        //颜色相关
        ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
        bb2.order(ByteOrder.nativeOrder());
        mColorBuffer = bb2.asFloatBuffer();
        mColorBuffer.put(mColor);
        mColorBuffer.position(0);
    }

绘制

我们在onDrawFrame的生命周期中进行绘制。

代码语言:javascript复制
    @Override
    public void onDrawFrame(GL10 gl) {

        Log.d("GLRender2" , "call onDrawFrame");
        // 清除屏幕和深度缓存
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        // 重置当前的模型观察矩阵
        gl.glLoadIdentity();

        // 允许设置顶点
        //GL10.GL_VERTEX_ARRAY顶点数组
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 允许设置颜色
        //GL10.GL_COLOR_ARRAY颜色数组
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        //将画笔在z轴上移动
        gl.glTranslatef(0f, 0.0f, -2.0f);

        // 设置三角形
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
        // 设置三角形颜色
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
        // 绘制三角形
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

        // 取消颜色设置
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
        // 取消顶点设置
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

        //绘制结束
        gl.glFinish();

    }

绘制的过程比较模式化,不再赘述。大致包含了:

  • 清除缓存
  • 启动顶点数组模式
  • 启动颜色数组模式
  • 移动画笔
  • 设置图形
  • 设置颜色
  • 关闭顶点数组模式
  • 关闭颜色数组模式
  • 绘制结束

至此,便完成了一个平平无奇的三角形的绘制过程。

如有问题,欢迎指正。

0 人点赞