EGL&OpenGL着色语言及案例

1,004 阅读14分钟

前言:

上篇案例:利用GLKit实现旋转立方体中,使用了苹果提供的GLKit框架,它提供的函数和类,包括提供加载纹理、数学计算、常用着色器、视图、视图控制器等,大大减少了工作量。但是GLKit也存在一定缺陷,着色器代码无法修改,着色器提供的属性变量有上限等。


EGL(Embedded Graphics Library)

OpenGL ES命令需要渲染上下文绘制表面才能完成图形图像的绘制,但是,OpenGL ES API并没有提供如何创建渲染上下文,或者上下文如何连接到原生窗口系统的方法。 EGL是Khronos渲染API和原生窗口系统之间的接口.但是iOS并不支持EGL,我们在iOS需要使用苹果封装的EAGL。

  • 渲染上下文:存储相关OpenGL ES状态,状态机
  • 绘制表面:用于绘制图元的表现,它指定渲染所需要的缓存区类型,例如颜色缓存区,深度缓存区,模版缓存区

EGL主要功能

  1. 和本地窗口系统进行通讯
  2. 查询可用的配置
  3. 创建OpenGL ES可用的绘图表面-CAEAGLayer
  4. 同步不同类别的API之间的渲染,比如在OpenGL ES和OpenVG之间同步,或者OpenGL和本地窗口的绘图命令
  5. 管理‘渲染资源’,比如纹理映射(rendering map)

OpenGL着色语言-GLSL

图形硬件的趋势是在那些变得极其复杂的领域,用可编程能力来代替固定功能,这样的两个领域就是顶点处理和片元处理。使用GLSL,顶点处理和片元处理的固定功能阶段因为有了可编程阶段而得到补充。

GLSL源自于C语言,同样包含了丰富的类型,包括向量和矩阵

向量数据类型

常用vec2,vec3,vec4浮点向量

类型描述
vec2,vec3,vec42分量,3分量,4分量浮点向量
ivec2,ivec3,ivec42分量、3分量、4分量整型向量
uvec2,uvec3,uvec42分量、3分量、4分量⽆符号整型向量
bvec2,bvec3,bvec42分量、3分量、4分量bool型向量

矩阵数据类型

常用mt3,mt4

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

GLSL数据修饰符

修饰符表示,是通过何种方式输入数据。

1. uniform

从app代码传递到顶点/片元着色器(vertex,fragment)中所用到的变量

  • 在vertex,fragment中会将uniform当成常量
  • 使用glUniform..进行传递,一般会通过uniform传递视图矩阵、投影矩阵,投影视图矩阵等
  • 在vertex和fragment中声明一样的类型、变量,就可以让vertex和fragment都能接收到。

2.attribute

只能从客户端传递到顶点着色器,并且只可以在顶点着色器中进行使用的数据

  • 传递数据包括:顶点、纹理坐标、颜色、法线等一切与坐标颜色有关的数据
  • 使用glVertex..进行传递,例如之前的glVertexAttribPointer
  • 使用attribute传递数据,在iOS一定要记得打开通道开关,iOS出于性能的考虑默认是关闭的

3.varying

当需要将顶点着色器的数据传递到片元着色器时,两个着色器中一模一样的数据变量就需要它来修饰

着色器代码简读

在iOS中,通过新建一个空白的类来编写着色器源码,通常用.vsh.fsh后缀分别代表顶点着色器和片元着色器,这样的后缀只是开发者用来区分,而Xcode实际并没有这样后缀的文件。

顶点着色器

// 通过attribute传递的4分量浮点数据,表示顶点数据
attribute vec4 position;     
// 通过attribute传递的2分量浮点数据,表示纹理坐标
/*
纹理坐标数据,是通过代码传递给vertex后,由vertex桥接给fragment
*/
attribute vec2 textCoordinate;
// 通过varying传递低精度的2分量浮点数据,表示纹理坐标
varying lowp vec2 varyTextCoord;

void main()
{
    // 数据桥接
    varyTextCoord = textCoordinate;
    // vertex计算之后的顶点数据需要赋值给GLSL的内建变量`gl_Position`
    gl_Position = position;
}

片元着色器

//定义精度,否则可能会报错
precsion highp float;
//纹理坐标 必须与顶点着色器中一模一样,通过这个参数获取传递过来的值
varying lowp vec2 varyTextCoord;
//纹理  sampler采样器, 
uniform sampler2D colorMap;   

void main(){
    //拿到纹理对应坐标下的纹素。纹素是纹理对应像素点的颜色值
    lowp vec4 temp = texture2D(colorMap, varyTextCoord);
    //纹理结果赋值给内建变量:gl_FragColor
    gl_FragColor = temp;
} 
  • 片元着色器中最终的颜色,即纹理坐标对应的纹素,需要通过内建函数textrue2D(纹理,纹理坐标)计算
  • 通过sampler采样器这样的数据类型,可以将纹理对象传递给片元着色器,1D、2D、3D代表不同维度的纹理类型,传递的其实是纹理id

着色器与程序的创建、编译、链接API

1.自定义着色器

  • 创建着色器
// 要创建的类型type:GL_VERTEX_SHADER / GL_FRAGMENT_SHADER
GLuint glCreateShader(GLenum type)
  • 将着色器源码附加到着色器对象上
/*
1.shader:要编译的着色器对象
2.count:源码字符串数量
3.string:源码内容
4.length:一般NULL,意味字符串是NULL终止的
*/
glShaderSource(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length)

在xcode创建空文件,自定义编写着色器代码,本质是一个字符串。

  • 编译着色器源码
void glCompileShader(GLuint shader);

2.program自定义程序

  • 创建
GLUint glCreateProgram()
  • 着色器与程序连接/附着
void glAttachShader(GLuint program,GLuint shader);
  • 链接程序
glLinkProgram(GLuint program)
  • 使用程序
glUseProgram(GLuint program)

以上API,基本表现出了,获取链接后着色器对象的一般过程:

  1. 创建一个顶点着色器对象和片元着色器对象
  2. 将着色器源码连接到每个着色器对象
  3. 编译着色器对象
  4. 创建一个程序对象
  5. 将编译后的着色器对象链接到程序对象
  6. 链接程序对象

案例:渲染一张图片

大致可以分为六个步骤

  • 设置图层:在iOS上用来绘制OpenGL ES内容CAEAGLLayer,继承CALayer
  • 创建上下文:EAGLContext用来保存状态
  • 清空缓冲区:以防有遗留数据造成干扰
  • 设置RenderBuffer:
  • 设置FrameBuffer

以上步骤为渲染做的准备工作,重点在于渲染的步骤,其中也可以继续分四步

  • 开始渲染
    • 清空Buffer,设置视口、背景颜色
    • 创建、编译、链接着色器
    • 处理顶点、纹理数据
    • 开始绘制,渲染屏幕

1.设置图层

+(Class)layerClass
{
    return [CAEAGLLayer class];
}
//1.设置图层
- (void)setupLayer{
    // 1 创建特殊图层
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    
    // 2.设置scale ,由屏幕的scale来决定
    [self setContentScaleFactor: [[UIScreen mainScreen]scale]];
    
    //3.设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
    /*
     kEAGLDrawablePropertyRetainedBacking  表示绘图表面显示后,是否保留其内容。
     kEAGLDrawablePropertyColorFormat 可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
     
         kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
         kEAGLColorFormatRGB565:16位RGB的颜色,
     kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。
     */
    self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@YES,kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
}

其中做layer强转时,需要重写类方法layerClass

2.设置context

// 2.设置图形上下文
- (void)setupContext{
    // 初始化2.0
    EAGLContext *context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
    if (context == nil) {
        NSLog(@"创建上下文失败");
        return;
    }
    
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"设置当前上下文失败");
        return;
    }
    self.myContext = context;
}

3.清空缓冲区

// 3.清空缓冲区
- (void)deleteRenderAndFrameBuffer{
    
    // 帧缓冲区 是 渲染缓冲区的管理者  FBO:frame buffer object
    // 先清空渲染缓冲区 , 然后帧缓冲区,未免有遗留脏数据,先做清空,再使用
    glDeleteRenderbuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
    glDeleteFramebuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
    
}

备注:以下会对帧缓冲区,渲染缓冲区做扩展

4.设置渲染缓冲区

因为帧缓冲区管理着渲染缓冲区,所以要先创建设置渲染缓冲区,然后在创建设置帧缓冲区时,建立两者间的管理关系

gen 申请分配缓冲区->bind 绑定缓冲区类型GL_RENDERBUFFER->renderbufferStorage 将缓冲区绑定到上下文

// 4.开辟渲染缓冲区
- (void)setupRenderBuffer{
    
    //1.定义bufferid
    GLuint buffer;
    // 申请缓冲区,得到bufferid存在buffer
    glGenRenderbuffers(1, &buffer);
    self.myColorRenderBuffer = buffer;
    // 绑定缓冲区
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    // 缓冲区绑定到上下文
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    
}

5.设置帧缓冲区

设置帧缓冲区时,需要和渲染缓冲区进行绑定

// 5.帧缓冲区
- (void)setupFrameBuffer{
    GLuint buffer;
    glGenFramebuffers(1, &buffer);
    self.myColorFrameBuffer = buffer;
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    // !!! 渲染缓冲区和 帧缓冲区 绑定一起
    /*
     glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
     target:GL_FRAMEBUFFER
     attachment:渲染缓冲区附着到帧缓冲区的哪里,GL_COLOR_ATTACHMENT0
     renderbuffertarget:
     */
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorFrameBuffer);
}

6.开始渲染

到此为止,准备工作告一段落,正式渲染工作中,也需要对开发者自己利用GLSL语言编写的着色器源码进行处理、以及顶点纹理数据的处理等.

6.1 基础设置

glClearColor(0.3, 0.45, 0.5, 1);
glClear(GL_COLOR_BUFFER_BIT);
    
// 视口
CGFloat scale = [[UIScreen mainScreen]scale];
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);

6.2 自定义着色器

利用上边的顶点着色器源码和片元着色器源码,在编写GLSL着色器源码时除了注意不同的修饰符代表的传输方式,也要注意不要在源码中留下注释,以免出现问题定位不准,毕竟它们只是个字符串

如上记录的:着色器与程序的创建、编译、链接API,我们需要对编写的着色器源码进行同样的处理,最终得到的程序来使用.

6.2.1 编译着色器源码得到着色器对象

其中用到的API都在上边做过了详细的介绍

- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
    
    // 源码读取路径,转换为NSString
    NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    // EAGL使用的是C语言函数,将OC的content字符串 转换为 C语言的字符串
    const GLchar *source = (GLchar *)[content UTF8String];
    
    // 1.创建对应类型的着色器
    *shader = glCreateShader(type);
    
    // 2.将着色器的源码 附着在着色器对象
    glShaderSource(*shader, 1, &source, NULL);
    
    // 3.编译
    glCompileShader(*shader);
    
}

  • 参数shader:表示传入的着色器对象,最终源码编译的结果就附着在着色器对象上
  • 参数type:表示创建的是什么类型的着色器
  • file:表示传入的着色器源码文件的位置

这个函数方法,通过我们传入的shader对象、着色器type,源码位置,最终得到一个附着有着色源码的对象

6.2.2 将着色器对象附着program程序来使用
- (GLuint)loaderShaders:(NSString *)vert withFrag:(NSString *)frag{
    
    // 1.定义顶点/片元着色器
    GLuint verShader,fragShader;
    
    // 2.定义一个program程序
    GLuint program = glCreateProgram();
    
    
    // 3.编译着色器
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    
    // 4.编译好的着色器对象 附着到程序
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    // 5.已经附着的shader对象可以删掉
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}
  • 参数vert,frag就是着色器源码的位置
  • 该函数调用成功后,我们还需要做glLinkProgra进行链接,链接结果可以通过glGetProgramiv获取
GLint linkStatus;
glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkStatus);

linkStatus成功的话,我们就可以调用glUseProgram来使用这个着色器程序

6.3 顶点、纹理数据

同样的,我们需要将顶点、纹理数据从内容中copy到缓冲区里

// 准备顶点/纹理数据
//前3个是顶点坐标,后2个是纹理坐标
GLfloat attrArr[] =
{
    0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
    -0.5f, -0.5f, -1.0f,    0.0f, 0.0f,
    
    0.5f, 0.5f, -1.0f,      1.0f, 1.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
    0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
};

// 将内存中的数据 copy 到缓冲区
// 顶点数据处理
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
// 打开顶点数据通道
// 从program获得顶点数据通道id position
GLuint position = glGetAttribLocation(self.myProgram, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);

// 打开纹理数据通道,传输纹理坐标数据
GLuint textCoordinate = glGetAttribLocation(self.myProgram, "textCoordinate");
glEnableVertexAttribArray(textCoordinate);
glVertexAttribPointer(textCoordinate, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL+3);

  • 其中有一点和GLKit有所不同,在GLKit中,苹果已经写好了着色器源码,当我们需要打开Attrib属性通道时,可以直接调用glEnableVertexAttribArray(GLKVertexAttribPosition);其中的GLKVertexAttribPosition,其实就是上面代码中GLuint position = glGetAttribLocation(self.myProgram, "position");的结果

  • 接下来还有一处与GLKit不同的地方:纹理加载.在GLKit中我们是使用GLKTextureLoader加载的图片纹理,那我们现在自定义的话需要另外自己写纹理加载的部分,这部分其实和OpenGL没有关系,它使用的是iOS中Core Graphics


//从图片中加载纹理
- (GLuint)setupTexture:(NSString *)fileName {
    
    //1、将 UIImage 转换为 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    //判断图片是否获取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    //2、读取图片的大小,宽和高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    //3.获取图片字节数 宽*高*4(RGBA)
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    //4.创建上下文
    /*
     参数1:data,指向要渲染的绘制图像的内存地址
     参数2:width,bitmap的宽度,单位为像素
     参数3:height,bitmap的高度,单位为像素
     参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
     参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
     参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
     */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    

    //5、在CGContextRef上--> 将图片绘制出来
    /*
     CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
     CGContextDrawImage 
     参数1:绘图上下文
     参数2:rect坐标
     参数3:绘制的图片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
   
    //6.使用默认方式绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
   
    //7、画图完毕就释放上下文
    CGContextRelease(spriteContext);
    
    //8、绑定纹理到默认的纹理ID(
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //9.设置纹理属性
    /*
     参数1:纹理维度
     参数2:线性过滤、为s,t坐标设置模式
     参数3:wrapMode,环绕模式
     */
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    float fw = width, fh = height;
    
    //10.载入纹理2D数据
    /*
     参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
     参数2:加载的层次,一般设置为0
     参数3:纹理的颜色值GL_RGBA
     参数4:宽
     参数5:高
     参数6:border,边界宽度
     参数7:format
     参数8:type
     参数9:纹理数据
     */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    //11.释放spriteData
    free(spriteData);   
    return 0;
}

纹理数据加载成功后,我们还需要将纹理数据,通过uniform传递给片元着色器,uniform修饰的数据,不需要打开通道开关

glUniform1i(glGetUniformLocation(self.myProgram,"colorMap"), 0);

6.4 开始绘制,显示

//绘图
glDrawArrays(GL_TRIANGLES, 0, 6);    
//从渲染缓存区显示到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];


FrameBuffer、RenderBuffer

  • ⼀个renderbuffer 对象是通过应⽤分配的⼀个2D图像缓存区。renderbuffer 能够被⽤来分配和存储颜⾊、深度或者模板值。也能够在⼀个framebuffer被⽤作颜⾊、深度、模板的附件。⼀个renderbuffer是⼀个类似于屏幕窗⼝系统提供可绘制的表⾯。⽐如pBuffer。⼀个renderbuffer,然后它并不能直接的使⽤像⼀个GL 纹理。
  • ⼀个 frameBuffer 对象(通常被称为⼀个FBO)。是⼀个收集颜⾊、深度和模板缓存区的附着点。描述属性的状态,例如颜⾊、深度和模板缓存区的⼤⼩和格式,都关联到FBO(Frame Buffer Object)。并且纹理的名字和renderBuffer 对象也都是关联于FBO。各种各样的2D图形能够被附着framebuffer对象的颜⾊附着点。它们包含了renderbuffer对象存储的颜⾊值、⼀个2D纹理或⽴⽅体贴图。或者⼀个mip-level的⼆维切⾯在3D纹理。同样,各种各样的2D图形包含了当时的深度值可以附加到⼀个FBO的深度附着点钟去。唯⼀的⼆维图像,能够附着在FBO的模板附着点,是⼀个renderbuffer对象存储模板值。


ps:为什么图片是反的?