OpenGLES简单入门

OpenGL

一、 基础概念

1. 简介

Android 可通过开放图形库 (OpenGL)(特别是 OpenGL ES API)来支持高性能 2D 和 3D 图形。OpenGL 是一种跨平台的图形 API,用于为 3D 图形处理硬件指定标准的软件接口。OpenGL ES 是 OpenGL 规范的一种形式,适用于嵌入式设备。

2. 坐标系

OpenGL ES中图形的位置是通过坐标系确定的,图形的绘制方式由点、线、三角形构成。在OpenGL中,世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的。当我们面对屏幕,右边是x正轴,上面是y正轴,屏幕指向我们为z正轴。长度单位如下:窗口范围按此单位是(-1,-1)到(1,1),即屏幕左下角坐标为(-1,-1),右上角 坐标为(1,1)。

3.着色器

着色器是用来描述顶点或像素特性的程序,它使用 GLSL(OpenGL Shading Language)语言开发,利用这种语言编写程序可运行在GPU上进行图像的处理或者渲染。着色器分为两个部分,即Vertex Shader(顶点着色器)与Fragment Shader(片元着色器)两部分,分别完成各自在OpenGL渲染管线中的功能,顶点着色器的主要目的就是确定每个顶点的最终位置,而片段着色器的主要目的就是告诉GPU每个片段的最终颜色应该是什么。

4.状态机

状态机是一种行为,即对象在其生命周期中响应事件所经历的状态序列以及对那些状态事件的响应。具有以下特点:

  • 有记忆功能,能够记住当前的状态
  • 可以接收输入,根据输入的内容和自己的原先状态,修改自己当前状态,并且可以有对应输出
  • 当进入特殊状态(停机状态)的时候,便不再接收输入,即停止工作

类推到OpenGL中,可以这么理解

  • OpenGL可以记录自己的状态(如当前所使用的颜色、是否开启了混合功能等)
  • OpenGL可以接收输入(调用OpenGL函数时,可以理解成OpenGL在接收输入),如调用glColor3f,即OpenGL接收到这个输入后,会修改自己的“当前颜色”这个状态
  • OpenGL可以进入停止状态,不再接收输入。在程序退出前,OpenGL总会先停止工作

5.OpenGL渲染

人眼所看到的图像是由屏幕上的像素点组成的,在内存中,这些像素点可以组织成一个大的一维数组,每4个Byte即表示一个像素点的RGBA数据,而在显卡中,这些像素点可以组织成帧缓冲区(FrameBuffer)的形式,帧缓冲区保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。
渲染的整体流程:
1. 指定几何对象:选择以何种方式绘制几何图形,例如点,面,三角形等。
2. 顶点处理:通过顶点着色器将本地坐标转换为坐标系中的坐标值。
3. 组装图元:在处理过顶点之后,各个顶点被按照绘制命令中的规则组装成图元。
4. 光栅化操作:图元数据在此被分成更小的单元并且对应于帧缓冲区的像素,一个单元成为片元。即从顶点数据到显示到设备上的像素的过程。
5. 片元处理:通过片元着色器计算每一一个片元的颜色值。
6. 帧缓冲操作:执行帧缓冲的写入操作,负责将最终的像素值写到帧缓冲区中。帧缓冲区简单理解就是存储屏幕上一帧画面的区域。

流程如下图:
流程.jpg

二、案例-绘制图片纹理

创建两个GLSL代码段-顶点着色器,片元着色器

OpenGL ES实现3D绘图和普通的2D绘图即view利用canvas来绘制不一样,OpenGL需要加载GLSL程式,让GPU进行绘制。所以需要定义shader代码,并在初始化的时候加载。

vertex_shader.glsl

attribute vec4 av_Position;//顶点位置
attribute vec2 af_Position;//纹理位置
varying vec2 v_texPo;//纹理位置与fragment_shader交互
void main() {
    v_texPo = af_Position;
    gl_Position = av_Position;
}

fragment_shader.glsl

precision mediump float;//精度 为float
varying vec2 v_texPo;//纹理位置接收于vertex_shader
uniform sampler2D sTexture;//纹理
void main() {
    gl_FragColor=texture2D(sTexture, v_texPo);
}

申明顶点/纹理坐标及其缓冲数组

    //顶点坐标
    var vertexData = floatArrayOf(
        -1f, -1f, 
        1f, -1f,
        -1f, 1f, 
        1f, 1f
    )

    //纹理坐标  对应顶点坐标与之映射
    var textureData = floatArrayOf(
        0f, 1f,
        1f, 1f,
        0f, 0f,
        1f, 0f
    )

init {
        //这个buffer是本地代码中用于存储顶点矩阵数据
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.size * 4)//内存大小为vertexData.size*4 顶点都存储在一个浮点数组里,每个浮点有4个字节
            .order(ByteOrder.nativeOrder())//字节缓冲区按照本地字节序组织它的内容
            .asFloatBuffer()//FloatBuffer类实例 避免直接操作字节
            .put(vertexData)//把数据从Dalvik的内存复制到本地内存
        vertexBuffer.position(0) //设置缓冲区起始位置

        //分配纹理坐标缓冲
        textureBuffer = ByteBuffer.allocateDirect(textureData.size * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(textureData)
        textureBuffer.position(0)
    }

加载GLSL代码并将其链接到程式中

调用glCreateShader()方法-传入渲染器类型参数type,创建对应的着色器对象;
调用glShaderSource()方法-传入着色器对象和字符串shaderCode定义的源代码,将二者关联起来;
调用glCompileShader()方法-传入着色器对象,对其进行编译。

 override fun onSurfaceCreated() {
        //创建程式 加载顶点着色器
        program = ShaderUtils.createProgram(
            context.resources,
            "vertex_shader.glsl",
            "fragment_shader.glsl"
        ).also {
            //获取顶点坐标字段
            avPosition = GLES32.glGetAttribLocation(it, "av_Position")
            //获取纹理坐标字段
            afPosition = GLES32.glGetAttribLocation(it, "af_Position")
            //
            bitmapTexture = GLES32.glGetUniformLocation(it, "sTexture")
            //创建要显示的图片纹理
            createTextureId(bitmap)
        }
    }

    /**
     * 创建着色器程序对象
     *
     * @param vertexShaderCode   :顶点着色器代码
     * @param fragmentShaderCode :片元着色器代码
     * @return program
     */
    private fun createProgram(
        vertexShaderCode: String?,
        fragmentShaderCode: String?
    ): Int {
        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        if (vertexShader == 0) {
            return 0
        }
        val fragmentShader =
            loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        if (fragmentShader == 0) {
            return 0
        }
        //创建空的程式
        var program = GLES20.glCreateProgram()
        if (0 != program) {
            //关联两个渲染器
            GLES20.glAttachShader(program, vertexShader)
            GLES20.glAttachShader(program, fragmentShader)
            GLES20.glLinkProgram(program)
            val linkStatus = IntArray(1)
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
            if (linkStatus[0] != GLES20.GL_TRUE) {
                GLES20.glDeleteProgram(program)
                program = 0
            }
        }
        return program
    }
    
    /**
     * 加载编译顶点渲染器
     *
     * @param type      :shader类型
     * @param shadeCode :着色器代码
     * @return shader
     */
    private fun loadShader(type: Int, shadeCode: String?): Int {
        var shader = GLES20.glCreateShader(type)
        if (0 != shader) {
            GLES20.glShaderSource(shader, shadeCode)
            GLES20.glCompileShader(shader)
            val compiled = IntArray(1)
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0)
            if (compiled[0] == 0) {
                GLES20.glDeleteShader(shader)
                shader = 0
            }
        }
        return shader
    }

创建图片纹理ID

fun createTextureId(bitmap: Bitmap?): Int {
        var result = 0
        bitmap?.let {
            //生产一个纹理
            val textureIds = IntArray(1)
            GLES32.glGenTextures(1, textureIds, 0)
            //绑定为 2D纹理
            GLES32.glBindTexture(GLES32.GL_TEXTURE_2D, textureIds[0])
            //设置环绕模式
            GLES32.glTexParameteri(GLES32.GL_TEXTURE_2D, GLES32.GL_TEXTURE_WRAP_S, GLES32.GL_REPEAT)
            GLES32.glTexParameteri(GLES32.GL_TEXTURE_2D, GLES32.GL_TEXTURE_WRAP_T, GLES32.GL_REPEAT)
            //设置过滤模式
            GLES32.glTexParameteri(
                GLES32.GL_TEXTURE_2D,
                GLES32.GL_TEXTURE_MIN_FILTER,
                GLES32.GL_LINEAR
            )
            GLES32.glTexParameteri(
                GLES32.GL_TEXTURE_2D,
                GLES32.GL_TEXTURE_MAG_FILTER,
                GLES32.GL_LINEAR
            )
            //绑定 bitmap到 textureIds[0] 这个2D纹理上
            GLUtils.texImage2D(GLES32.GL_TEXTURE_2D, 0, bitmap, 0)

            val bitmapBuffer = ByteBuffer.allocate(bitmap.height * bitmap.width * 4)
            bitmap.copyPixelsToBuffer(bitmapBuffer)
            //将bitmapBuffer位置移动到初始位置
            bitmapBuffer.flip()
            //设置内存大小绑定内存地址
            GLES32.glTexImage2D(
                GLES32.GL_TEXTURE_2D, 0, GLES32.GL_RGBA, bitmap.width, bitmap.height,
                0, GLES32.GL_RGBA, GLES32.GL_UNSIGNED_BYTE, bitmapBuffer
            )
            //退出 纹理的设置,进入下一环节
            GLES32.glBindTexture(GLES32.GL_TEXTURE_2D, 0)
            result = textureIds[0]
        }
        return result
    }

绘制纹理到屏幕上

override fun onDrawFrame() {
        //清空颜色
        GLES32.glClear(GLES32.GL_COLOR_BUFFER_BIT)
        //设置背景颜色
        GLES32.glClearColor(1f, 1f, 1f, 1f)
        //使用GLSL程式
        GLES32.glUseProgram(program)
        //绘制视频源  流程为->配置、绑定id、释放
        GLES32.glEnableVertexAttribArray(avPosition)
        GLES32.glEnableVertexAttribArray(afPosition)
        //顶点着色器的纹理/顶点坐标
        GLES32.glVertexAttribPointer(avPosition, 2, GLES32.GL_FLOAT, false, 0, vertexBuffer)
        GLES32.glVertexAttribPointer(afPosition, 2, GLES32.GL_FLOAT, false, 0, textureBuffer)
        //传入图片纹理
        GLES32.glBindTexture(GLES32.GL_TEXTURE_2D, bitmapTexture)
        GLES32.glDrawArrays(GLES32.GL_TRIANGLE_STRIP, 0, 4)
        //释放
        GLES20.glDisableVertexAttribArray(avPosition)
        GLES20.glDisableVertexAttribArray(afPosition)
        GLES32.glBindTexture(GLES32.GL_TEXTURE_2D, 0)
        GLES32.glBindBuffer(GLES32.GL_ARRAY_BUFFER, 0)
    }

三、更多

GLSL基础语法

  • GLSL是一种面向过程的语言,和Java的面向对象是不同的。

  • GLSL的基本语法与C/C++基本相同。

  • 它完美的支持向量和矩阵操作。

  • 它是通过限定符操作来管理输入输出类型的。(in、out)

  • GLSL提供了大量的内置函数来提供丰富的扩展功能。

  • GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:

//标量(只有大小没有方向)
float a=1.0;
int b=1;
bool c=true;

vec2 d=vec2(1.0,2.0);
vec3 e=vec3(1.0,2.0,3.0)
vec4 f=vec4(vec3,1.2);
vec4 g=vec4(0.2);  //相当于vec(0.2,0.2,0.2,0.2)
vec4 h=vec4(a,a,1.3,a);
//矩阵
mat2 i=mat2(0.1,0.5,1.2,2.4);
mat2 j=mat2(0.8);   //相当于mat2(0.8,0.8,0.8,0.8)
mat3 k=mat3(e,e,1.2,1.6,1.8);
  • 类型转换
    GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1;也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float。

  • 限定符

  • attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。

  • uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。

  • varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。

  • const:常量。

浮点精度

image-20200408225702185
  • lowp:低精度。8位。
  • mediump:中精度。10位。
  • highp:高精度。16位。

内建变量

gl_Position: 用于vertex shader, 写顶点位置;被图元收集、裁剪等固定操作功能所使用;
           其内部声明是:highp vec4 gl_Position;
gl_PointSize: 用于vertex shader, 写光栅化后的点大小,像素个数;
           其内部声明是:mediump float gl_Position;
gl_FragColor: 用于Fragment shader,写fragment color;被后续的固定管线使用;
            mediump vec4 gl_FragColor;
gl_FragData: 用于Fragment shader,是个数组,写gl_FragData[n] 为data n;被后续的固定管线使用;
            mediump vec4 gl_FragData[gl_MaxDrawBuffers];
gl_FragColor和gl_FragData是互斥的,不会同时写入;
gl_FragCoord: 用于Fragment shader,只读, Fragment相对于窗口的坐标位置 x,y,z,w; 这个是固定管线图元差值后产生的;z 是深度值; mediump vec4 gl_FragCoord;
gl_FrontFacing: 用于判断 fragment是否属于 front-facing primitive;只读;
              bool gl_FrontFacing;   
gl_PointCoord: 仅用于 point primitive; mediump vec2 gl_PointCoord;

推荐阅读更多精彩内容