OpenGL ES - 了解GLSL

2,459 阅读9分钟

前言

上个教程了解了苹果官方为减轻OpenGL ES开发提供的一个简单库GLKit。我们也知道GLKit能提供的纹理通道只有两个,光照效果三个,在需要更多的纹理以及自定义效果只能通过GLSL(Open GL Shader Language)来实现。

版本对比

也许很多学习的人会纠结OpenGL ES用啥版本,当真正使用的时候就不会有这种疑问。会发现各个版本有个版本的针对情况,实际使用可以根据需求来进行一个选择。

OpenGL ES 版本 描述
1.x 针对固定管线硬件
2.x 针对可编程管线硬件
3.0 对2.0版本的扩充
3.1 在3.0的基础上增加了几何着色器

初步了解GLSL

GLSL就是为了开发者编写顶点着色器和片元着色器的语言。在之前的OpenGL中我们知道可以使用固定管线进行编程,也就是使用默认的顶点着色器和片元着色器,所以实现自定义的效果必须使用可编程管线。GLSL是一门C风格的语言,也有着控制结构(if、for、switch等),其实我们了解了GLSL的规范后开发还是很容易的,但是因为编译器不会提示,只会在一些关键字写对后会进行一个色彩显示,也就意味着我们在写着色器的时候必须很仔细,不然会出现编译着色器失败。

GLSL的基本知识

向量类型

类型 描述
vec2 vec3 vec4 二维、三维、四维浮点向量
ivec2 ivec3 ivec4 二维、三维、四维整形向量
uvec2 uvec3 uvec4 二维、三维、四维无符号整形向量
bvec2 bvec3 bvec4 二维、三维、四维布尔型向量

矩形数据类型

类型 描述
mat2 mat2x2 两行两列
mat3 mat3x3 三行三列
mat4 mat4x4 四行四列
mat2x3 三行两列
mat2x4 四行两列
mat3x2 两行三列
mat3x4 四行三列
mat4x2 两行四列
mat4x3 三行四列

变量存储限定符

限定符 描述
<none> 普通的本地变量,外部不可见并访问
const 一个编译常量,只读属性
in/varying 从以前阶段传递过来的变量
in/vartying centroid 一个从以前处理阶段传递过来的变量,使用质心插值
out/attribute 传递下一处理阶段或者在一个函数中指定一个返回值
out/attribute centroid 传递到下一个处理阶段,质心插值
uniform 一个从客户端传递过来的变量,在顶点之间不做改变

顶点着色器代码

#version 300 es // 通常会写这个版本号,假如OpenGL ES 3.3.0 则为330
attribute vec4 position;		// 顶点数据都是用attribute修饰,用attribute通道进行传值
attribute vec2 textureCootdinate;		// 纹理顶点数据
varying lowp vec2 textureCoord;			// 需要传递给片元着色器的纹理顶点,且需要精度修饰符

void main(){
	textureCoord = textureCootdinate;
  gl_Position = position;
}

我们需要注意的点有以下这些:

①#version 是为了告诉OpenGL ES版本号,必须处于空行外的第一行。书写GLSL时每行结束需要添加分号和换行。

②顶点数据都需要使用attribute修饰,用于attribute通道进行传值。而且顶点数据需要用vec4修饰,因为顶点是RGBA四个数据,否则编译着色器将会报错。

③传递给片元着色器的纹理顶点需要使用varying修饰,只有使用了varying修饰的属性才能传递到片元着色器;其次还需使用精度修饰符,精度修饰符有lowpmediumphighp低中高三种精度。

④着色器源码中只能有一个main函数。

⑤在main函数中把需要传递给片元着色器的值进行赋值。例如纹理属性值

必须对内建变量gl_Position进行赋值

片段着色器代码

#version 300 es
varying lowp vec2 textureCoord;
uniform sampler2D textureSampler;

void main(){
  	gl_FragColor = texture2D(textureSampler, textureCoord);
}

代码和顶点着色器差不多,因为片段着色器是处理纹理数据的,所以一些贴图、滤镜大部分都在这里进行。片元着色器的内建变量是gl_FragColor

常见的错误

我们知道在OpenGL ES开发中会通过glGetError函数来获取错误代码,来排查错误原因,其中在开发中如果粗心或者一不留神写错了值就容易造成12801286错误代码。

错误 代码 原因
GL_NO_ERROR 0(0x0)
GL_INVALID_ENUM 1280(0x0500) 使用了不合法的枚举
GL_INVALID_VALUE 1281(0x0501) 数值参数超出范围
GL_INVALID_OPERATION 1282(0x0502) 操作在当前状态非法
GL_OUT_OF_MEMORY 1285(0x0505) 内存不足以执行命令
GL_INVALID_FRAMEBUFFER_OPERATION 1286(0x0506) 帧缓冲区不完整

FBO和RBO

1.为何使用帧缓冲区对象(FBO)?

**在应用调用任何OpenGL ES命令之前,需要首先创建一个渲染上下文和绘图表面,并使之成为现行的上下文和表面。**渲染上下文和绘图表面通常由原生窗口系统通过EGL等API提供,我们知道在iOS中是EAGL。由原生系统提供的绘图表面可以是屏幕上显示的表面(称为系统提供的帧缓冲区);也可以是屏幕外的表面(称为pbuffer)。创建CAEAGLLayer绘图表面的我们可以指定宽度、高度以及是否使用颜色、深度和模板缓冲区以及这些缓冲区的位深。

在默认情况,在iOS中OpenGL ES使用CAEAGLayer作为绘图表面。如果应用程序只在屏幕上的表面绘图,则窗口系统提供的帧缓冲区童话村那个很高效。但是,因为许多应用程序需要渲染到纹理,为此,使用系统提供的帧缓冲区作为绘图表面通常不是理想的选择。渲染到纹理的实例由阴影贴图、动态反射和环境贴图、多道景深技术、运动模糊效果和处理后特效。

应用程序可以通过以下两种方式渲染到纹理:

通过绘制窗口系统提供的缓冲区,然后将缓冲区的对应区域复制到纹理来实现渲染到纹理。这可以用glCopyTexImage2DglCopyTexSubImage2D API实现。这些API执行从帧缓冲区到纹理缓冲区的复制,之一复制操作往往对性能有不利影响。此方法只适用于纹理的尺寸小于或者等于帧缓冲区尺寸的时候才有效。

通过使用连接到纹理的pbuffer来实现渲染到纹理。我们知道,窗口系统提供的表面必须连接到一个渲染上下文。这在某些对每个pbuffer和窗口表面需要不同的上下文实现中可能效率低下。可绘制表面切换有时候需要OpenGL ES清除所有切换之前渲染的图像。

但是以上两种方式对于渲染到纹理或者其他屏幕外表面来说都不理想。作为替代,我们需要允许程序直接渲染到纹理的API,或者在OpenGL ES API中具备创建屏幕外表面的能力,并将它作为渲染目标。帧缓冲区对象和渲染缓冲区对象允许应用程序完成这些操作,不需要额外创建渲染上下文。因为我们在使用窗口系统提供了渲染到纹理或者屏幕外表面的更好、更有效的方法。

2.帧缓冲区对象API支持如下操作

①仅使用OpenGL ES命令创建帧缓冲区对象。

②在单一EAGLContext中创建和使用多个帧缓冲区对象。也就是说不需要每个帧缓冲区都有一个渲染上下文。

③创建屏幕外颜色、深度、或者模板渲染缓冲区和纹理,并将它们连接到帧缓冲区对象。

④在每个帧缓冲区之间共享颜色、深度或者模板缓冲区。

⑤将纹理直接连接到帧缓冲区作为颜色或深度,从而避免了进行复制操作的必要操作。

⑥在帧缓冲区之间复制并使用帧缓冲区内容失效

3.FBO和RBO关系

帧缓冲区对象(FBO)是一组颜色、深度、模板纹理或者渲染目标对象。各种2D图像可以连接到帧缓冲区对象中的颜色附着点。这些附着点包括一个渲染缓冲区对象,他保存颜色值、2D纹理或者立方图面的mip级别、2D数组纹理的层次甚至3D纹理中一个2D切片的mip级别;同样的包含深度值得各种2D图像可以连接到FBO的深度附着点;可以连接到FBO模板附着点的唯一2D图像是保存模板值得渲染缓冲区对象。而且一个帧缓冲区只有一个颜色、深度、模板附着点。

渲染缓冲区对象(RBO)是一个有应用程序分配的2D图像缓冲区。渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作帧缓冲区对象中的颜色、深度或者模板附着。渲染缓冲区类似于屏幕外的窗口系统提供的可绘制表面,例如pbuffer但是渲染缓冲区不能直接作为GL的纹理。

案例:使用glsl纹理贴图

根据上面的流程图我们知道和GLKit不同的是使用自定义GLSL代替了GLKBaseEffect部分内容。值得注意的是我们程序正常的加载纹理是倒像,我们可以通过处理顶点数据或者处理纹理映射方式来处理,但是都不是很好的处理办法。较好的做法是在加载纹理的时候重绘位图时进行一个转换操作:

CGRect rect = CGRectMake(0, 0, imageWidth, imageHeight);
CGContextTranslateCTM(context, 0, imageHeight);
CGContextScaleCTM(context, 1.0f, -1.0f);

实现的效果图:

案例demo地址:Swift版本OC版本