OpenGL ES简介详解手机开发

概述

在聊Android的View渲染流程中,通常会有一个比较核心的步骤:通过OpeGL ES接口调用GPU接口通知GPU绘制图形。其完整的流程:UI对象—->CPU处理为多维图形,纹理 —–通过OpeGL ES接口调用GPU—-> GPU对图进行光栅化(Frame Rate ) —->硬件时钟(Refresh Rate)—-垂直同步—->投射到屏幕。

这里写图片描述

详解的绘制原理,后面会慢慢讲的。

渲染的基础知识

使用OpenGL ES,一般包括如下几个步骤:

  (1)EGL初始化
  (2)OpenGL ES初始化
  (3)OpenGL ES设置选项&绘制
  (4)OpenGL ES资源释放(可选)
  (5)EGL资源释放

Android提供的GLSurfaceView和Renderer自动完成了(1)(5)两个部分,这部分只需要开发者做一些简单配置即可。另外(4)这一步是可选的,因为随着EGL中上下文的销毁,openGL ES用到的资源也跟着释放了。因此只有(2)(3)是开发者必须做的。这大大简化了开发过程,但是灵活性也有所降低,利用这两个类是无法完成offscreen render的。要想完成offscreen render其实也很简单,相信大家也都猜到了,只要我们把(1)~(5)都自己完成就可以了。后续部分的代码大部分都是C/C++,少部分是Java。

初始化

EGL的功能是将OpenGL ES API和设备当前的窗口系统粘合在一起,起到了沟通桥梁的作用。不同设备的窗口系统千变万化,但是OpenGL ES提供的API却是统一的,所以EGL需要协调当前设备的窗口系统和OpenGL ES。下面EGL初始化的代码我是用C++写的,然后通过jni调用。Android在Java层面上也提供了对应的Java接口函数。

static EGLConfig eglConf; 
static EGLSurface eglSurface; 
static EGLContext eglCtx; 
static EGLDisplay eglDisp; 
 
JNIEXPORT void JNICALL Java_com_handspeaker_offscreentest_MyGles_init 
(JNIEnv*env,jobject obj) 
{ 
    // EGL config attributes 
    const EGLint confAttr[] = 
    { 
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,// very important! 
            EGL_SURFACE_TYPE,EGL_PBUFFER_BIT,//EGL_WINDOW_BIT EGL_PBUFFER_BIT we will create a pixelbuffer surface 
            EGL_RED_SIZE,   8, 
            EGL_GREEN_SIZE, 8, 
            EGL_BLUE_SIZE,  8, 
            EGL_ALPHA_SIZE, 8,// if you need the alpha channel 
            EGL_DEPTH_SIZE, 8,// if you need the depth buffer 
            EGL_STENCIL_SIZE,8, 
            EGL_NONE 
    }; 
    // EGL context attributes 
    const EGLint ctxAttr[] = { 
            EGL_CONTEXT_CLIENT_VERSION, 2,// very important! 
            EGL_NONE 
    }; 
    // surface attributes 
    // the surface size is set to the input frame size 
    const EGLint surfaceAttr[] = { 
             EGL_WIDTH,512, 
             EGL_HEIGHT,512, 
             EGL_NONE 
    }; 
    EGLint eglMajVers, eglMinVers; 
    EGLint numConfigs; 
 
    eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY); 
    if(eglDisp == EGL_NO_DISPLAY) 
    { 
        //Unable to open connection to local windowing system 
        LOGI("Unable to open connection to local windowing system"); 
    } 
    if(!eglInitialize(eglDisp, &eglMajVers, &eglMinVers)) 
    { 
        // Unable to initialize EGL. Handle and recover 
        LOGI("Unable to initialize EGL"); 
    } 
    LOGI("EGL init with version %d.%d", eglMajVers, eglMinVers); 
    // choose the first config, i.e. best config 
    if(!eglChooseConfig(eglDisp, confAttr, &eglConf, 1, &numConfigs)) 
    { 
        LOGI("some config is wrong"); 
    } 
    else 
    { 
        LOGI("all configs is OK"); 
    } 
    // create a pixelbuffer surface 
    eglSurface = eglCreatePbufferSurface(eglDisp, eglConf, surfaceAttr); 
    if(eglSurface == EGL_NO_SURFACE) 
    { 
        switch(eglGetError()) 
        { 
        case EGL_BAD_ALLOC: 
        // Not enough resources available. Handle and recover 
            LOGI("Not enough resources available"); 
            break; 
        case EGL_BAD_CONFIG: 
        // Verify that provided EGLConfig is valid 
            LOGI("provided EGLConfig is invalid"); 
            break; 
        case EGL_BAD_PARAMETER: 
        // Verify that the EGL_WIDTH and EGL_HEIGHT are 
        // non-negative values 
            LOGI("provided EGL_WIDTH and EGL_HEIGHT is invalid"); 
            break; 
        case EGL_BAD_MATCH: 
        // Check window and EGLConfig attributes to determine 
        // compatibility and pbuffer-texture parameters 
            LOGI("Check window and EGLConfig attributes"); 
            break; 
        } 
    } 
    eglCtx = eglCreateContext(eglDisp, eglConf, EGL_NO_CONTEXT, ctxAttr); 
    if(eglCtx == EGL_NO_CONTEXT) 
    { 
        EGLint error = eglGetError(); 
        if(error == EGL_BAD_CONFIG) 
        { 
            // Handle error and recover 
            LOGI("EGL_BAD_CONFIG"); 
        } 
    } 
    if(!eglMakeCurrent(eglDisp, eglSurface, eglSurface, eglCtx)) 
    { 
        LOGI("MakeCurrent failed"); 
    } 
    LOGI("initialize success!"); 
}

代码比较长,不过大部分都是检测当前函数调用是否出错的,核心的函数只有6个,只要它们的调用没有问题即可:

eglGetDisplay(EGL_DEFAULT_DISPLAY)

eglInitialize(eglDisp, &eglMajVers, &eglMinVers)

eglChooseConfig(eglDisp, confAttr, &eglConf, 1, &numConfigs)

eglCreatePbufferSurface(eglDisp, eglConf, surfaceAttr)

eglCreateContext(eglDisp, eglConf, EGL_NO_CONTEXT, ctxAttr)

eglMakeCurrent(eglDisp, eglSurface, eglSurface, eglCtx)

OpenGL ES初始化

JNIEXPORT void JNICALL Java_com_handspeaker_offscreentest_MyGles_draw 
(JNIEnv*env,jobject obj) 
{ 
    const char*vertex_shader=vertex_shader_fix; 
    const char*fragment_shader=fragment_shader_simple; 
    glPixelStorei(GL_UNPACK_ALIGNMENT,1); 
    glClearColor(0.0,0.0,0.0,0.0); 
    glEnable(GL_DEPTH_TEST); 
    glDepthFunc(GL_LESS); 
    glCullFace(GL_BACK); 
    glViewport(0,0,512,512); 
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); 
    glShaderSource(vertexShader,1,&vertex_shader,NULL); 
    glCompileShader(vertexShader); 
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 
    glShaderSource(fragmentShader,1,&fragment_shader,NULL); 
    glCompileShader(fragmentShader); 
    GLuint program = glCreateProgram(); 
    glAttachShader(program, vertexShader); 
    glAttachShader(program, fragmentShader); 
    glLinkProgram(program); 
    glUseProgram(program); 
    GLuint aPositionLocation =glGetAttribLocation(program, "a_Position"); 
    glVertexAttribPointer(aPositionLocation,2,GL_FLOAT,GL_FALSE,0,tableVerticesWithTriangles); 
    glEnableVertexAttribArray(aPositionLocation); 
    //draw something 
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
    glDrawArrays(GL_TRIANGLES,0,6); 
    eglSwapBuffers(eglDisp,eglSurface); 
}

EGL资源释放

JNIEXPORT void JNICALL Java_com_handspeaker_offscreentest_MyGles_release 
(JNIEnv*env,jobject obj) 
{ 
    eglMakeCurrent(eglDisp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); 
    eglDestroyContext(eglDisp, eglCtx); 
    eglDestroySurface(eglDisp, eglSurface); 
    eglTerminate(eglDisp); 
 
    eglDisp = EGL_NO_DISPLAY; 
    eglSurface = EGL_NO_SURFACE; 
    eglCtx = EGL_NO_CONTEXT; 
}

OpenGL ES在Android中的应用

为了让你的控件能够显示在界面上,你必须创建一个view作为容器。而要想创建View容器,最直接的方式莫过于从GLSurfaceView和GLSurfaceView.Renderer分别派生一个类,实际的绘图动作都是在GLSurfaceView.Renderer里面发生的。对于一个全屏或近全屏的graphicsview,它是最好的选择。如果只是在某个小部分显示OpenGLES图形则可以考虑TextureView。当然你也可以直接继承自OpenGLES view创建一个View,不过一般都不会这么做。

OpenGL ES 的Android实例

1,在Manifest中声明使用OpenGLES

为了能使用OpenGLES 2.0 API,你必须在你的manifest中添加以下声明:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

如果你的应用要使用纹理压缩功能,还必须声明设备需要支持什么样的压缩格式:

<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" /> 
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

2,创建一个Activity

这个Activity和普通的activity一样,不过其使用的布局layout需要使用GLSurfaceView包裹。

<?xml version="1.0" encoding="utf-8"?> 
<GLSurfaceView xmlns:android="http://schemas.android.com/apk/res/android"> 
     .... 
</GLSurfaceView> 
 

注:OpenGL ES 2.0需要Android2.2 (API Level 8) 及以上版本。

3,构键GLSurfaceView对象

GLSurfaceView中其实不需要做太多工作,实际的绘制任务都在GLSurfaceView.Renderer中了。这里我们可以直接使用GLSurfaceView。

class MyGLSurfaceView extends GLSurfaceView { 
 
    public MyGLSurfaceView(Context context){ 
        super(context); 
        //设置Renderer到GLSurfaceView 
        setRenderer(new MyRenderer()); 
    } 
}

当使用OpenGLES 2.0时,你必须在GLSurfaceView构造器中调用另外一个函数,它说明了你将要使用2.0版的API:

setEGLContextClientVersion(2);

另一个可以添加的你的GLSurfaceView实现的可选的操作是设置render模式为只在绘制数据发生改变时才绘制view。使用GLSurfaceView.RENDERMODE_WHEN_DIRTY:

setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

4, 构建一个Renderer类

Renderer类主要负责GLSurfaceView的绘制工作,它主要有三个方法:

  • onSurfaceCreated()- 仅调用一次,用于设置view的OpenGLES环境。
  • onDrawFrame()- 每次View被重绘时被调用。
  • onSurfaceChanged()- 如果view的几和形状发生变化了就调用,例如当竖屏变为横屏时。

如我们要在GLSurfaceView上画了一个灰色的背景。

public class MyGL20Renderer implements GLSurfaceView.Renderer { 
 
    public void onSurfaceCreated(GL10 unused, EGLConfig config) { 
        //设置背景的颜色 
        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); 
    } 
 
    public void onDrawFrame(GL10 unused) { 
        // 重绘背景色 
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 
    } 
 
    public void onSurfaceChanged(GL10 unused, int width, int height) { 
        GLES20.glViewport(0, 0, width, height); 
    } 
}

OpenGL ES渲染原理

首先来看一个OpenGL ES2.0的渲染原理图。

这里写图片描述

1 VBO/VAO

VBO/VAO是cpu提供给GPU的顶点信息,包括了顶点的位置、颜色、纹理坐标(用于纹理贴图)等顶点信息。
VBO,全名Vertex Buffer Object。它是GPU里面的一块缓冲区,当我们需要传递数据的时候,可以先向GPU申请一块内存,然后往里面填充数据。最后,再通过调用glVertexAttribPointer把数据传递给Vertex Shader。
VAO,全名为Vertex Array Object,它的作用主要是记录当前有哪些VBO,每个VBO里面绑定的是什么数据,还有每一个vertex attribute绑定的是哪一个VBO。

2 VertexShader(顶点着色器)

顶点着色器的输入数据由下面组成:

  • Attributes:使用顶点数组封装每个顶点的数据,一般用于每个顶点都各不相同的变量,如顶点位置、颜色等
  • Uniforms:顶点着色器使用的常量数据,不能被着色器修改,一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的变量,如当前光源的位置。
  • Samplers:这个是可选的,一种特殊的uniforms,表示顶点着色器使用的纹理。
  • Shader program:顶点着色器的源码或可执行文件,描述了将对顶点执行的操作。

顶点着色器的输出:

  • varying:在图元光栅化阶段,这些varying值为每个生成的片元进行计算,并将结果作为片元着色器的输入数据。从分配给每个顶点的原始varying值来为每个片元生成一个varying值的机制叫做插值。
  • 另外,还有gl_postion、gl_FrontFacing和gl_PointSize。

顶点着色器可用于传统的基于顶点的操作,例如:基于矩阵变换位置,进行光照计算来生成每个顶点的颜色,生成或者变换纹理坐标。
另外因为顶点着色器是由应用程序指定的,所以你可以用来进行任意自定义的顶点变换。

3 PrimitiveAssembly(图元装配):

顶点着色器下一个阶段是图元装配,这个阶段,把顶点着色器输出的顶点组合成图元。图元(primitive)是一个能用opengl es绘图命令绘制的几何体,包括三角形、直线或者点精灵等几何对象,绘图命令指定了一组顶点属性,描述了图元的几何形状和图元类型。在图元装配阶段,这些着色器处理过的顶点被组装到一个个独立的几何图元中,例如三角形、线、点精灵。对于每个图元,必须确定它是否位于视椎体内(3维空间显示在屏幕上的可见区域),如果图元部分在视椎体中,需要进行裁剪,如果图元全部在视椎体外,则直接丢弃图元。裁剪之后,顶点位置转换成了屏幕坐标。背面剔除操作也会执行,它根据图元是正面还是背面,如果是背面则丢弃该图元。经过裁剪和背面剔除操作后,就进入渲染流水线的下一个阶段:光栅化。

4 rasterization(光栅化)

这里写图片描述

光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理(片段着色器的输入)。这些二维片段代表着可在屏幕上绘制的像素。用于从分配给每个图元顶点的顶点着色器输出生成每个片段值的机制称作插值(Interpolation)。这句不是人话的话解释了一个问题,就是从cpu提供的分散的顶点信息是如何变成屏幕上密集的像素的,图元装配后顶点可以理解成变为图形,光栅化时可以根据图形的形状,插值出那个图形区域的像素(纹理坐标v_texCoord、颜色等信息)。注意,此时的像素并不是屏幕上的像素,是不带有颜色的。接下来的片段着色器完成上色的工作。总之,光栅化阶段把图元转换成片元集合,之后会提交给片元着色器处理,这些片元集合表示可以被绘制到屏幕的像素。

5 FragmentShader(片段着色器)

这里写图片描述

片段着色器为片段(像素)上的操作实现了通用的可编程方法,光栅化输出的每个片段都执行一遍片段着色器,对光栅化阶段生成每个片段执行这个着色器,生成一个或多个(多重渲染)颜色值作为输出。
片元着色器对片元实现了一种通用的可编程方法,它对光栅化阶段产生的每个片元进行操作,需要的输入数据如下:

  • Varying variables:顶点着色器输出的varying变量经过光栅化插值计算后产生的作用于每个片元的值。
  • Uniforms:片元着色器使用的常量数据
  • Samplers:一种特殊的uniforms,表示片元着色器使用的纹理。
  • Shader program:片元着色器的源码或可执行文件,描述了将对片元执行的操作。

片元着色器也可以丢弃片元或者为片元生成一个颜色值,保存到内置变量gl_FragColor。光栅化阶段产生的颜色、深度、模板和屏幕坐标(Xw, Yw)成为流水线中pre-fragment阶段(FragmentShader之后)的输入。

6Per-Fragment Operations(逐个片元操作阶段)

这里写图片描述

片元着色器之后就是逐个片元操作阶段,包括一系列的测试阶段。一个光栅化阶段产生的具有屏幕坐标(Xw, Yw)的片元,只能修改framebuffer(帧缓冲)中位置在(Xw, Yw)的像素。

上图显示了Opengl es 2.0逐片元操作过程:

  • Pixel ownership test:像素所有权测试决定framebuffer中某一个(Xw,Yw)位置的像素是否属于当前Opengl ES的context,比如:如果一个Opengl ES帧缓冲窗口被其他窗口遮住了,窗口系统将决定被遮住的像素不属于当前Opengl ES的context,因此也就不会被显示。
  • Scissor test:裁剪测试决定位置为(Xw, Yw)的片元是否位于裁剪矩形内,如果不在,则被丢弃。
  • Stencil and depth tests:模板和深度测试传入片元的模板和深度值,决定是否丢弃片元。
  • Blending:将新产生的片元颜色值和framebuffer中某个(Xw, Yw)位置存储的颜色值进行混合。
  • Dithering:抖动可以用来最大限度的减少使用有限精度存储颜色值到framebuffer的工件。
  • 逐片元操作之后,片元要么被丢弃,要么一个片元的颜色,深度或者模板值被写入到framebuffer的(Xw,Yw)位置,不过是否真的会写入还得依赖于write masks启用与否。write masks能更好的控制颜色、深度和模板值写入到合适的缓冲区。例如:颜色缓冲区中的write mask可以被设置成没有红色值写入到颜色缓冲区。另外,Opengl ES 2.0提framebuffer中获取像素的接口,不过需要记住的是像素只能从颜色缓冲区读回,深度和模板值不能读回。

参考:
OpenGL渲染流程 http://www.cnblogs.com/BigFeng/p/5068715.html
OpenGL ES 2.0渲染管线 http://codingnow.cn/opengles/1504.html
OpenGL ES 2.0可编程管道 http://www.cnblogs.com/listenheart/p/3292672.html
OpenGL ES 2.0编程基础 http://blog.csdn.net/iispring/article/details/7649628
OpenGL-渲染管线的流程 http://www.cnblogs.com/zhanglitong/p/3238989.html

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5868.html

(0)
上一篇 2021年7月17日 00:21
下一篇 2021年7月17日 00:21

相关推荐

发表回复

登录后才能评论