Android OpenGL基础——Shader的使用流程

https://rainmonth.github.io/posts/A210122.html

概览

Android通过Open Graphics Library(OpenGL®),特别是OpenGL ES API包括对高性能2D和3D图形的支持。OpenGL是一种跨平台的图形API,它为3D图形处理硬件指定了标准的软件接口。OpenGL ES是用于嵌入式设备的OpenGL规范的一种形式。Android支持以下几种版本的OpenGL ES API:

  • OpenGL ES 1.0 and 1.1,从1.0起就开始支持;
  • OpenGL ES 2.0,从Android2.0(API Level 8)开始支持;
  • OpenGL ES 3.0,从Android4.3(API Level 18)开始支持;
  • OpenGL ES 3.1,从Android5.0(API Level 21)开始支持;

也就是说目前主流Android 4.4以上的设备都可以使用OpenGL ES 3.0了。

系列文章

关于OpenGL一些概念的详细说明,参见下面文章:

注意:设备能支持OpenGL ES API的前提是设备制造商提供了相应管道的实现,不然一切免谈。

基础

Android中开发OpenGL的流程:

  • OpenGL运行准备工作(也就是GLSurfaceView的设置工作)
  • Shader源码的编写及编译及对象创建;
  • Program的创建,Shader的挂在,Program的编译链接使用及销毁等;
  • 调试运行程序

GLSurfaceView

GLSurfaceView是OpenGL在Android中的载体(因为OpenGL是一个图形库,但它没有实现自己的图形窗口话系统,需要依赖于当前环境来提供它渲染的载体,Android中这一工作就是通过GLSurfaceView来实现的。

Android 中主要通过GLSurfaceView以及GLSurfaceView.Rendereer来使用OpenGL ES API

  • GLSurfaceView,继承自SurfaceView,采用一个专门的Surface来展示OpenGL的渲染,有以下特性:
    1. 管理Surface,这是可以 合成到Android视图系统中的特殊内存空间;
    2. 管理EGL显示,使OpenGL能够渲染到Surface中;
    3. 接受用户提供的渲染对象,进行实际的渲染;
    4. 在专用线程上进行渲染,以将渲染性能与UI线程分离;
    5. 支持按需渲染和连续渲染;
    6. 可选地包装,跟踪和/或错误检查渲染器的OpenGL调用;

OpenGL ES 绘制图形

OpenGL支持绘制的图形包括三种

  1. 线
  2. 三角形

其他复杂的图形都是通过上面三种基础图形来绘制的。

坐标系统

OpenGL坐标轴正方向定义:

朝右为X轴正方向,朝上为Y周正方向,朝外为Z轴正方向

OpenGL中,我们最终看到的坐标系统是展示在标准化设备坐标NDC(Normalized Device Coordinate)中。这个坐标系统中,所有的坐标值都在(-1,1)之间,坐标原点相当于是在正方体的中心。所以将OpenGL ES的坐标系映射到Android设备时,如果不进行矩阵变化,就会变形。这个变形就是一个3D->2D的过程;经历的坐标矩阵变化就是如下过程:

  1. 物体在局部空间(Local Space)存在一个对应的局部坐标(Local Coordinate);
  2. 然后这个局部坐标(Local Coordinate)会转换成世界坐标(Word Coordinate);
  3. 然后世界坐标将转换成观察空间坐标(View Coordinate)(即每个坐标点都是相对于摄像机或观察者的);
  4. 得到观察控件坐标(View Coordinate)后,我们需要按实际情况通过投影矩阵(Projection Matrix)进行变换得到裁剪坐标(Clip Coordinate),这一步会确认那些点最终会出现在屏幕上;
  5. 最后,裁剪坐标会被转换成屏幕坐标,通过视口变换(Viewport Transform)将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

所以,一个OpenGL物体需要经过四次矩阵变换才能最终显示在终端屏幕上。

小结参考:

一般步骤

一般步骤见下图:

Shader使用的一般步骤

  • Create Program,对应glCreateProgram
  • Load Shader,加载Shader,主要包括下满三步:
    • Create Shader,对应glCreateShader(shaderType),创建shader对象,shaderType在源码中定义了两种,分别是:
      • GL_VERTEX_SHADER,顶点Shader;
      • GL_GEOMETRY_SHADER,几何Shader,顶点到片段之间的过渡;
      • GL_FRAGMENT_SHADER,片段Shader;
    • Source Shader,对应glShaderSource(GLuint shader, GLsizei count, const GLchar * const *string, const GLint *length),替换Shader对象中的Shader 源程序
    • Compile Shader,对应glCompileShader(shader),编译Shader源码
    • 获取Shader对象相关的信息,对应glGetShaderiv,查询Shader对象的参数信息,支持查询以下信息:
      • GL_SHADER_TYPE, 获取Shader的类型;
      • GL_DELETE_STATUS,获取Shader是否已经删除;
      • GL_COMPILE_STATUS,获取Shader是否已经编译;
      • GL_INFO_LOG_LENGTH,获取Shader的日志信息;
      • GL_SHADER_SOURCE_LENGTH,获取Shader的源码长度;
  • Attach Shader,对应glAttachShader,将Shader和Program attach起来;
  • Link Program,对应glLinkProgram,链接Program对象;
  • 获取Program对象的相关信息,对应glGetProgramiv,该方法和glGetShaderiv类似,具体支持的信息可以查看文档;
  • Use Program,对应glUseProgram,开始渲染Program对象的内容了(Installs a program object as part of current rendering state);
  • Dettach Shader,对应glDettachShader,从Program中detach Shader;
  • Delete Shader,对应glDeleteShader,删除 Shader对象;
  • Delete Program,对应glDeleteProgram,删除Program对象;

Program

调用glCreateProgram可以小创建一个Program,OpenGL中Program相当于当前渲染管线所使用的程序,是Shader的容器,可以挂载多个Shader。Program相关的函数有:

  • glCreateProgram,创建Program对象;
  • glAttachShader,将Program和Shader绑定;
  • glLinkProgram,链接Program;
  • glUseProgram,使用Program;
  • glDeleteProgram,删除Program;

辅助函数:

  • glGetProgramiv,获取Program对象,然后从Program随想根据key-value形式来获取随想信息;
  • glGetProgramInfoLog,获取Program对象的log信息

Shader

基本概念

Shader其实就是一段C程序,在按一定的语法编写好Shader源代码后,需要经过以下步骤得到Shader对象的句柄,然后通过该句柄将其与Program绑定后使用。

  • glCreateShader,创建Shader对象;
  • glShaderSource,替换Shader对象中的Shader源码;
  • glCompileShader,编译Shader源码;
  • glDettachShader,将Shader从Program解绑;
  • glDeleteShader,删除Shader对象;

Shader基本信息

类型

主要分一下三类:

  • GL_VERTEX_SHADER,顶点Shader;
  • GL_GEOMETRY_SHADER,几何Shader,顶点到片段之间的过渡;
  • GL_FRAGMENT_SHADER,片段Shader;

属性变量与统一变量

在Shader中,属性变量统一变量由应用程序设置,Attribute属性变量用于传递顶点信息,而Uniform统一变量则用于传递用户自定义的变量。这两种变量在Shader中会被定义为全局变量,在OpenGL中要设置这两种变量,就需要先获取它们的地址,然后调用OpenGL相关的设置接口为它们赋值。

属性变量设置

获取属性变量地址

1
2
3
4
5
/**
* @param program 属性变量所在Shader关联的Program对象句柄
* @param name 属性变量名称
*/
GLint glGetAttribLocation(GLuint program, char *name);

通过上面的方法获取到变量的位置后,才能通过调用glVertexAttribXx系列方法来设置设个属性变量的值。

设置属性变量

第一步,假设属性变量的名字为:attrA,要设置这个值,先或去这个变量的位置 locationAttrA:

1
GLint locationAttrA = glGetAttribLocation(program, "attrA");

第二步,在渲染开始前进行属性变量的赋值,这里有两种方式:

  1. 第一种是在glBegin()和glEnd()中间,在使用glVertex系列函数生成顶点前。先调用glVertexAttrib系列函数进行赋值,接下来生成的顶点会绑定前面设置的属性变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    glBegin(GL_TRIANGLE_STRIP);
    glVertexAttrib1f(locationAttrA, 1.0f);
    glVertex2f(0.0f, 0.0f);
    glVertexAttrib1f(locationAttrA, 2.0f);
    glVertex2f(0.0f, 1.0f);
    glVertexAttrib1f(locationAttrA, 3.0f);
    glVertex2f(1.0f, 1.0f);
    glVertexAttrib1f(locationAttrA, 4.0f);
    glVertex2f(1.0f, 0.0f);
    glEnd();
  2. 第二种是使用顶点数组渲染时,这个得先激活属性变量数组的功能,激活方法:

    1
    void glEnableVertexAttribArray(GLint locationAttrA);

    开启这个功能后,需要调用glVertexAttribPointer()方法,将属性变量的值批量传入,属性变量的数组和顶点数组是一一对应的。先看看该方法的说明:

    1
    2
    3
    4
    5
    6
    7
    //参数local,属性变量的位置
    //参数size,属性变量的分量数量,必须为1~4,如1为float、2~3为vec2~3
    //参数type,属性类型,如GL_FLOAT
    //参数normalized,是否对传入的值执行一次归一化操作
    //参数stride,顶点数组中,两个顶点之间的步幅,0表连续的顶点
    //参数pointer,属性变量列表指针,与顶点数组中的顶点一一对应
    void glVertexAttribPointer(GLint local, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);

    使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义4个顶点
float vertices[8] = {
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f
};
float myattributes[4] = {1.0f, 2.0f, 3.0f, 4.0f};

//获取一个已经成功链接的Program中的myattribute属性变量
GLint local = glGetAttribLocation(p, "myattribute");

//使用顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
//启用顶点属性变量数组
glEnableVertexAttribArray(local);
//设置顶点数组
glVertexPointer(2, GL_FLOAT, 0, vertices);
//设置顶点属性数组
glVertexAttribPointer(local, 1, GL_FLOAT, GL_FALSE, 0, myattributes);
统一变量设置

属性变量相当于每个顶点的私有只读变量,而Uniform统一变量则相当于整个Program的全局只读变量。统一变量和属性变量一样,都是先获取变量的位置,然后调用相关的接口进行设置的。不过统一变量在绘制时不能修改,所以必须在绘制前设置它的值。
至于设置接口和属性变量一致,只是将方法名中的Attrib或VertexAttrib替换成Uniform。统一变量的设置比属性变量要轻松得多,因为不需要想办法绑定到每个顶点上,只需要在渲染之前进行设置就可以了。

基本语法

VertexShader示例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 指定OpenGL ES的版本,没有写的话默认是2.0
#version es 300
// 定义一个统一变量矩阵
uniform mat4 u_matViewProj;
// 指定顶点属性索引为0的位置为四维向量
layout(location = 0) in vec4 a_position;
// 指定顶点属性索引为1的位置为三维向量
layout(location = 1) in vec3 a_color;
// 指定输出值为一个三维向量
smooth out vec3 v_color;

void main() {
// 指定gl_Position的计算方式
gl_Position = u_matViewProj * a_position;
// 输出v_color,并制定插值限定符为smooth
v_color = a_color;
}

说明:

  • 顶点着色器中,输入变量通常存储位置、法线、纹理坐标和颜色这样的数据,数据由应用程序加载

  • gl_Position,无需in、out、uniform等关键字的声明而直接使用,原来它是默认是归一化的裁剪空间坐标,xyz各个维度的范围为-1到1,仅能在顶点着色器中使用,既是输入也是输出gl_Position赋值范围就是float的取值范围(32位),只不过只有[-1,1]区间的片元被绘制。它是vec4类型的,不能重声明为dvec4等类型。

  • 插值限定符,主要有三种,分别为:smooth、flat、centroid,说明如下:

    • smooth,默认方式,平滑着色,顶点着色器的输出变量在图元中线性插值;

    • flat,平面着色,将一个顶点视为驱动顶点(取决于图元类型),该顶点的值用于图元中所有片段;

    • centroid,质心采样,使用多重采样渲染时,该限定符可用于强制插值发生在被渲染图元内部,否则图元边缘可能出现伪像;

FragmentShader示例说明

1
2
3
4
5
6
7
8
9
10
11
12
// 指定版本
#version es 300
// 指定精度
precision mediump float;
// 指定输入变量(通常来自顶点着色器中的输入变量
in vec3 v_color;
// 通过布局限定符指定输出一个颜色
layout(location = 0) out vec4 o_fragColor;

void main() {
o_fragColor = vec4(v_color, 1.0);
}

说明:

  • 精度限定符,有lowp(低精度)、mediump(中精度)和highp(高精度)三种,较低的精度效率更高,较高的精度效果更高,通过precision来指定精度,通常:
    • 顶点着色器中,如果没有指定默认的精度,则int和float默认都是highp;
    • 片段着色器中,浮点值没有默认的精度,必须由开发者指定;
  • 片段着色器的输入一般都是来自顶点着色器的输出;
  • 片段着色器通常会输出一个颜色(单目标渲染)或多个颜色(渲染多个目标MRT),具体的对应关系可以通过限定符layout来指定;

OpenGL ES 3.0 shader 着色语言基础语法

小结参考:

小结

本人主要介绍了以下Android OpenGL 中Shader相关的基础知识,大多是参看上面的那位大佬的文章,然后结合者自己看看OpenGL的文档,这样就当结识了OpenGL的Shader了.

编译错误及常见问题

  1. Expected token ‘{‘, found ‘identifier’?

    通常是GLSL语法错误,看看是不是GLSL内部类写错了;

  2. 在给Shader中的统一变量(Uniform)变量赋值时,需要先激活或使用Shader(即调用glUseProgram方法);

参考文章: