欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-20 14:54:46
...

整体思路

本文不采用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对象存储模板值。
    OpenGL ES之GLSL渲染图片显示的整体流程

  • 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渲染加载图片