OpenGL ES之GLSL渲染图片显示的整体流程

整体思路

本文不采用UIKit的GLKBaseEffect渲染一张图片的显示,而是使用编译链接自定义的着色器(shader)。用GLSL语言来实现自定义顶点/片元着色器,并将图形进行简单的渲染显示。

一、GLKit渲染
二、GLSL自定义着色器实现图片渲染的思路说明
  • 创建图层:重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer,并设置描述属性;
  • 创建上下文:上下文主要是用于保存OpenGL ES中的状态,是一个状态机,不论是GLKIt还是GLSL,都是需要context的;
  • 清空缓存区:buffer分为 frameBuffer 和 renderBuffer 两个大类,都需要清空;
  • 设置RenderBuffer:渲染缓存区
  • 设置FrameBuffer:帧缓存区
  • 开始绘制:读取顶点/片元着色器的程序,并加载shader,编译顶点着色程序/片元着色器程序,然后链接程序,设置顶点、纹理坐标,并处理顶点坐标和纹理。
三、GLSL着色语言

准备工作

一、ViewController和View
  • 新建一个view继承UIView,命名为YDWView;
  • 在ViewController中引入#import “YDWView.h”,并声明一个@property(nonatomic, strong)YDWView *myView属性。在viewDidLoad加载myView:
 - (void)viewDidLoad {
    [super viewDidLoad];
    
    self.myView = (YDWView *)self.view;
}
  • 在YDWView中定义部分需要用到的全局变量(在iOS和tvOS上绘制OpenGL ES内容的图层,继承CALayer),并重载layoutSubviews和重写layerClass方法(重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer):
@property (nonatomic, strong) CAEAGLLayer *myEagLayer;
@property (nonatomic, strong) EAGLContext *myContext;

@property (nonatomic, assign) GLuint myColorRenderBuffer;
@property (nonatomic, assign) GLuint myColorFrameBuffer;

@property (nonatomic, assign) GLuint myPrograme;

- (void)layoutSubviews {
    
}

// 重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer
+ (Class)layerClass {
    return [CAEAGLLayer class];
}

二、自定义着色器的vsh和fsh文件创建
  • 创建shader.vsh文件:
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main() {
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

  • 创建shaderv.fsh文件:
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main() {
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}


渲染流程

一、创建图层
  • 创建特殊图层并设置scale
    /* 创建特殊图层
     * 重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer
     */
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    // 设置scale
    [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
  • 设置描述属性,不维持渲染内容以及颜色格式为RGBA8;
    ① kEAGLDrawablePropertyRetainedBacking: 表示绘图表面显示后,是否保留其内容;
    ② kEAGLDrawablePropertyColorFormat :表示可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象,默认是kEAGLColorFormatRGBA8;
kEAGLDrawablePropertyColorFormat 说明
kEAGLColorFormatRGBA8 32位RGBA的颜色,4*8=32位
kEAGLColorFormatRGB565 16位RGB的颜色
kEAGLColorFormatSRGBA8 sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响
 self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false, kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
 
二、创建上下文
  • 创建图形上下文,指定OpenGL ES 渲染API版本
    // 创建图形上下文
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
  • 判断是否创建成功和设置图形上下文是否成功
    // 判断是否创建成功
    if (!context) {
        return;
    }
    // 判断设置图形上下文是否成功
    if (![EAGLContext setCurrentContext:context]) {
        return;
    }
  • 将局部context,变成全局的context
    // 将局部context,变成全局的context
    self.myContext = context;
三、清空缓存区
  • ⼀个 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对象存储模板值。
    在这里插入图片描述

  • buffer分为 frameBuffer 和 renderBuffer 两个大类,其中 frameBuffer 相当于 renderBuffer 的管理者;

  • frame buffer object 即称 FBO;

  • renderBuffer 则又可分为3类:colorBuffer(颜⾊缓存区)、depthBuffer(深度缓存区)、stencilBuffer(模板缓存区)。

	glDeleteBuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
    
    glDeleteBuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
四、设置RenderBuffer
    // 定义一个缓存区ID
    GLuint buffer;
    // 申请一个缓存区标志
    glGenBuffers(1, &buffer);
    
    // 将局部colorBuffer,变成全局的colorBuffer
    self.myColorRenderBuffer = buffer;
    // 将标识符绑定到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    // 将可绘制对象drawable object's  CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    
五、设置FrameBuffer
    // 定义一个缓存区ID
    GLuint buffer;
    // 申请一个缓存区标志
    glGenBuffers(1, &buffer);
    
    // 将局部frameBuffer,变成全局的frameBuffer
    self.myColorFrameBuffer = buffer;
    // 将标识符绑定到GL_RENDERBUFFER
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    // 将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到GL_COLOR_ATTACHMENT0上
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
    
  • 生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,绘制才能起作用。
六、开始绘制
  • 设置视口大小
    // 设置清屏颜色
    glClearColor(0.4f, 0.4f, 0.4f, 1.0f);
    // 清除颜色缓存区
    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);

  • 读取顶点着色程序、片元着色程序
    // 读取顶点着色程序、片元着色程序
    NSString *vertFile = [[NSBundle mainBundle] pathForResource:@"shader" ofType:@"vsh"];
    NSString *fragFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"fsh"];

    // 加载shader
    self.myPrograme = [self loadShaders:vertFile Withfrag:fragFile];
  • 加载shader
// 加载shader
- (GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag {
    // 定义2个临时着色器对象
    GLuint verShader, fragShader;
    GLint program = glCreateProgram();
    
    /* 编译顶点着色程序、片元着色器程序
     * 参数1:编译完存储的底层地址
     * 参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
     * 参数3:文件路径
     */
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    // 创建最终的程序
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    // 释放不需要的shader
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}

  • 编译顶点着色程序、片元着色器程序
// 编译顶点着色程序、片元着色器程序
 - (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file {
    // 读取文件路径字符串
    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar* source = (GLchar *)[content UTF8String];
    
    // 创建一个shader(根据type类型)
    *shader = glCreateShader(type);
    
    /* 将着色器源码附加到着色器对象上
     * 参数1:shader,要编译的着色器对象 *shader
     * 参数2:numOfStrings,传递的源码字符串数量 1个
     * 参数3:strings,着色器程序的源码(真正的着色器程序源码)
     * 参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
     */
    glShaderSource(*shader, 1, &source,NULL);
    
    // 把着色器源代码编译成目标代码
    glCompileShader(*shader);
}
  • 链接:判断是否链接成功
    // 链接
    glLinkProgram(self.myPrograme);
    GLint linkStatue;
    // 获取链接状态
    glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatue);
    
    if (linkStatue == GL_FALSE) {
        GLchar message[512];
        glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
        NSString *messageString = [NSString stringWithUTF8String:message];
        NSLog(@"Program Link Error:%@",messageString);
        return;
    }
  • 使用program
	// 使用program
    glUseProgram(self.myPrograme);
  • 设置顶点、纹理坐标,并处理顶点坐标和纹理
    // 设置顶点、纹理坐标,前3个是顶点坐标,后2个是纹理坐标
    GLfloat attrArray[] = {
        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,
    };
    
    /* 处理顶点数据
     * 1.顶点缓存区
     * 2.申请一个缓存区标识符
     * 3.将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
     * 4.把顶点数据从CPU内存复制到GPU上
     */
    GLuint attrBuffer;
    glGenBuffers(1, &attrBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArray), attrArray, GL_DYNAMIC_DRAW);

    /* 将顶点数据通过myPrograme中的传递到顶点着色程序的position
     * glGetAttribLocation,用来获取vertex attribute的入口的
     * 告诉OpenGL ES,通过glEnableVertexAttribArray
     * 最后数据是通过glVertexAttribPointer传递过去的
     */
    // 第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
    GLuint position = glGetAttribLocation(self.myPrograme, "position");

    // 设置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(position);

    /* 设置读取方式
     * 参数1:index,顶点数据的索引
     * 参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
     * 参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
     * 参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
     * 参数5:stride,连续顶点属性之间的偏移量,默认为0;
     * 参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
     */
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
    
    /* 处理纹理数据
     * glGetAttribLocation,用来获取vertex attribute的入口的
     * 第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
     */
    GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
    
    // 设置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(textCoor);
    
    /* 设置读取方式
     * 参数1:index,顶点数据的索引
     * 参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
     * 参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
     * 参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
     * 参数5:stride,连续顶点属性之间的偏移量,默认为0
     * 参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
     */
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
    
    // 加载纹理
    [self setupTexture:@"yiyi"];
  • 从图片中加载纹理
// 从图片中加载纹理
 - (GLuint)setupTexture:(NSString *)fileName {
    
    // 将 UIImage 转换为 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    // 判断图片是否获取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    // 读取图片的大小,宽和高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    // 获取图片字节数 宽*高*4(RGBA)
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    /* 创建上下文
     * 参数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);
    

    /* 在CGContextRef上将图片绘制出来
     * CGContextDrawImage 使用的是Core Graphics框架,坐标系与 UIKit 不一样
     * UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角
     * CGContextDrawImage
        参数1:绘图上下文
        参数2:rect坐标
        参数3:绘制的图片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
   
    // 使用默认方式绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
   
    // 画图完毕就释放上下文
    CGContextRelease(spriteContext);
    
    // 绑定纹理到默认的纹理ID(
    glBindTexture(GL_TEXTURE_2D, 0);
    
    /* 设置纹理属性
     * 参数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;
    
    /* 载入纹理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);
    
    // 释放spriteData
    free(spriteData);
    return 0;
}
  • 渲染绘制
    // 设置纹理采样器 sampler2D
    glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
    
    // 绘图
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    // 从渲染缓存区显示到屏幕上
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
  • 最后在layoutSubviews调用这些方法即可
 - (void)layoutSubviews {
    
    [self setupLayer];
    [self setupContext];
    [self clearRenderAndFrameBuffer];
    [self setupRenderBuffer];
    [self setupFrameBuffer];
    [self renderLayer];
}

  • 到这里,一张图片的渲染显示,就已经完成了。但是大家可能已经发现:渲染出来的图片怎么是倒着的呢?其实这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。所以这里提供一种解决方案:在shaderv.fsh文件中将gl_FragColor = texture2D(colorMap, varyTextCoord);改为:gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));即可。
  • 完整代码传送门:OpenGL_ES之GLSL渲染加载图片

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/107679738