美文网首页
OpenGL ES案例03 - 使用GLSL完成纹理图片加载

OpenGL ES案例03 - 使用GLSL完成纹理图片加载

作者: 卡布奇诺_95d2 | 来源:发表于2020-08-26 17:29 被阅读0次

    案例:根据对GLSL语言的理解,自定义一个顶点着色器和一个片元着色器,使用着色器API完成纹理的加载。
    进阶:解决纹理倒置问题。
    效果如下:


    翻转前效果
    翻转后效果

    准备工作

    1. 新建iOS应用工程,修改当前controller的view。将原来的view继承于UIView改成继承于HView。
    2. 自定义一个HVIew类,后续绘制图片在该类中完成。
    3. 新建顶点着色器文件和片元着色器文件。
      3.1 command + N,开始新建文件。
      3.2 选择iOS->Other->Empty,新建两个空文件,分别命名为:shaderv.vsh、shaderf.fsh

    至此准备工作完成,接下来就开始编码工作。

    自定义着色器

    自定义着色器本质上其实是一个字符串,但是在Xcode的编写过程没有任何错误提示,因此,在编写过程中需要格外仔细。

    1. 顶点着色器shaderv.vsh
    • 定义两个attribute修饰符修饰的变量,分别表示顶点坐标和纹理坐标
    • 定义一个varying修饰符修饰的变量,用于将纹理坐标从顶点着色器传递给片元着色器
    • main函数,在该函数内给内建变量gl_Position赋值。若顶点坐标不需要变换,则直接将顶点坐标赋值给内建变量gl_Position。若顶点坐标需要进行变换,则将变换后的结果赋值给内建变量gl_Position。
    attribute vec4 position;
    attribute vec2 textCoordinate;
    varying lowp vec2 varyTextCoord;
    void main(){
        varyTextCoord = textCoordinate;
        gl_Position = position;
    }
    

    2.片元着色器shaderf.fsh

    • 指定片元着色器中float类型的精度,如果不写,可能会报一些异常错误
    • 定义一个与顶点着色器桥接的纹理坐标,写法必须同在顶点着色器写法一致,否则将无法收到从顶点着色器传递过来的数据
    • 定义一个unifom修饰符修饰的变量,用于获取纹理坐标上每个像素点的纹素。
    • main函数,在函数内给内建变量gl_FragColor赋值。通过texture2D内建函数获取当前颜色值,它有两个参数:参数1:纹理图片;参数2:纹理坐标,返回值:vec4类型的颜色值。当颜色不需要进行修改时,可直接将vec4类型的颜色值赋值给内建变量gl_FragColor。当颜色需要修改时,将最终修改的结果赋值给内建变量gl_FragColor。
    precision highp float;
    varying lowp vec2 varyTextCoord;
    uniform sampler2D colorMap;
    void main(){
        gl_FragColor = texture2D(colorMap, varyTextCoord);
    }
    

    初始化

    1. 创建图层

    1.1 图层主要是显示OpenGL ES绘制内容的载体。它的创建有两种方式:
    • 直接使用当前view的layer。但是view的layer是继承于CALayer,需要重写类方法layerClass,使其继承于CAEAGLLayer
    • 直接使用[[CAEAGLLayer alloc] init]方法创建一个CAEAGLLayer类型的图层,并将新创建的图层添加到当前图层上。
    self. myEagLayer = (CAEAGLLayer*)self.layer;
    + (Class)layerClass{
        return [CAEAGLLayer class];
    }
    
    1.2 设置scale,这里设置当前view的scale与屏幕的scale一样大
    [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
    
    1.3 设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
    • kEAGLDrawablePropertyRetainedBacking:表示绘图表面显示后,是否保留其内容,true-保留,false-不保留
    • kEAGLDrawablePropertyColorFormat:可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
    颜色缓冲区格式 描述
    kEAGLColorFormatRGBA8 32位RGBA的颜色,4*8=32位
    kEAGLColorFormatRGB565 16位RGB的颜色
    kEAGLColorFormatSRGBA8 sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。
    self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false, kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatSRGBA8, kEAGLDrawablePropertyColorFormat, nil];
    

    2. 创建上下文

    上下文主要用来保存OpenGL ES的状态,是一个状态机,不论GLKit还是GLSL,都需要使用context。
    2.1 创建上下文,并指定OpenGL ES渲染API的版本号

    self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    

    2.2 设置当前上下文

    [EAGLContext setCurrentContext:self.myContext];
    

    3. 清空缓冲区

    清除缓冲区的残留数据,防止其它无用数据对绘制效果造成影响

    //清空渲染缓存区
    glDeleteBuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
        
    //清空帧缓存区
    glDeleteBuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
    

    4. 设置缓冲区

    设置缓冲区包括设置RenderBuffer和FrameBuffer。

    1. RenderBuffer:是一个通过应用分配的2D图像缓冲区,需要附着在FrameBuffer上。
      1.1 RenderBuffer有3种缓冲区

      • 深度缓冲区(Depth Buffer):存储深度值等
      • 纹理缓冲区(Depth Buffer):存储纹理坐标中对应的纹素、颜色值等
      • 模板缓冲区(Stencil Buffer):存储模板等

      1.2 设置RenderBuffer

      • 定义一个缓存区ID
      • 申请一个缓冲区标志
      • 将缓冲区标识绑定到GL_RENDERBUFFER
      • 绑定一个可绘制对象(layer)的存储到一个OpenGL ES RenderBuffer对象
    -(void)setupRenderBuffer{
        //1.定义一个缓存区ID
        GLuint buffer;
        //2.申请一个缓存区标志
        glGenRenderbuffers(1, &buffer);
        
        self.myColorRenderBuffer = buffer;
        
        glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
        
        [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    }
    
    1. FrameBuffer:是一个收集颜色、深度、模板缓冲区的附着点,简称FBO,即是一个管理者,用来管理RenderBuffer,且FrameBuffer没有实际的存储功能,真正实现存储的是RenderBuffer。
      2.1 FrameBuffer有3个附着点

      • 颜色附着点(Color Attachment):管理纹理、颜色缓冲区
      • 深度附着点(depth Attachment):管理深度缓冲区,会根据当前深度缓冲中的值修改颜色缓冲中的内容
      • 模板附着点(Stencil Attachment):管理模板缓冲区

      2.2 设置FrameBuffer

      • 定义一个缓存区ID
      • 申请一个缓冲区标志
      • 将缓冲区标识绑定到GL_FRAMEBUFFER
      • 通过FrameBuffer来管理RenderBuffer,将RenderBuffer附着到FrameBuffer的GL_COLOR_ATTACHMENT0附着点上。
    -(void)setupFrameBuffer{
        GLuint buffer;
        glGenFramebuffers(1, &buffer);
        
        self.myColorFrameBuffer = buffer;
        
        glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
        
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
    }
    

    注意点:绑定renderBuffer和FrameBuffer是有顺序的,先有RenderBuffer,才有FrameBuffer。

    开始绘制

    初始化

    清除屏幕颜色,清空颜色缓冲区,设置视口大小。

    //设置清屏颜色
    glClearColor(0.3, 0.45, 0.5, 1.0);
        
    //清除屏幕
    glClear(GL_COLOR_BUFFER_BIT);
    
    //1.设置视口大小
    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);
    

    加载自定义着色器

    1. 读取并编译顶点着色程序、片元着色程序

    1.1 创建一个顶点/片元着色器

    *shader = glCreateShader(type);
    

    1.2 以字符串的形式将着色器源码读取出来,并将着色器源码加载到着色器对象上

    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar* source = (GLchar*)content.UTF8String;
    glShaderSource(*shader, 1, &source, NULL);
    

    1.3 编译着色器,把着色器源代码编译成目标代码。此时得到一个可附着到程序的着色器对象

    glCompileShader(*shader);
    
    2. 加载着色器

    2.1 创建program

    GLint program = glCreateProgram();
    

    2.2 将编译好的着色器对象附着到程序中

    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    

    2.3 释放不需要的着色器对象

    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    1. 链接program
      在链接之后可调用glGetProgramiv函数判断当前是否链接成功
    glLinkProgram(self.myPrograme);
    
    1. 使用program
    glUseProgram(self.myPrograme);
    

    设置并处理顶点数据

    1. 设置顶点数据
    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,
    };
    
    1. 申请一个顶点缓冲区ID,并将它绑定到GL_ARRAY_BUFFER标识符上
    GLuint attrBuffer;
    glGenBuffers(1, &attrBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    
    1. 把顶点数据从CPU拷贝到GPU
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
    
    1. 打开顶点/片元着色器属性通道
    • 通过glGetAttribLocation函数获取顶点属性入口,它需要两个参数,参数1:program;参数2:自定义着色器文件中变量名称的字符串,重点:这里的字符串必须同自定义着色器文件中变量名称保持一致
    • 通过glEnableVertexAttribArray函数打开着色器的属性通道
    • 通过glVertexAttribPointer函数设置读取方式
    //设置顶点坐标
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    glEnableVertexAttribArray(position);
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), NULL);
    
    //设置纹理坐标
    GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
    glEnableVertexAttribArray(textCoor);
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (float *)NULL + 3);
    

    加载纹理

    加载纹理的过程是将png/jpg图片解压缩成位图,并通过自定义着色器读取每个像素点的纹素。

    1. 解压缩png/jpg图片,将UIImage转换为CGImageRef。
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    1. 根据CGImageRef属性获取图片的宽和高,并开辟一段空间用于存放解压缩后的位图信息。位图数据的大小为宽4。为什么是宽4?因为图片共有宽高个像素点,每个像素点有4个字节,即RGBA,因此共有宽高*4大小的空间。
    //读取图片的大小,宽和高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    //获取图片字节数 宽*高*4(RGBA)
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    1. 创建CGContextRef上下文
    /*
     参数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);
    
    1. 在CGContextRef上将图片绘制出来,调用CGContextDrawImage函数,使用默认方式绘制
    /*
     CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
     CGContextDrawImage 
     参数1:绘图上下文
     参数2:rect坐标
     参数3:绘制的图片
    */
    CGContextDrawImage(spriteContext, rect, spriteImage);
    
    1. 绘制完成之后,需要将上下文释放掉
    CGContextRelease(spriteContext);
    
    1. 经过重绘之后,就将jpg/png图片转换成了位图得到了纹理数据。接下来就是载入纹理数据。
      6.1 绑定纹理到默认的纹理ID
      6.2 设置纹理属性
      6.3 载入2D纹理数据
    //绑定纹理到默认的纹理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);
    
    /*
     参数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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    //释放spriteData
    free(spriteData); 
    
    1. 设置纹理采样器
      主要是用来获取纹理中对应像素点的颜色值,即纹素。
    • 通过glGetUniformLocation函数获取片元着色器中uniform的入口。该函数需要传入两个参数,参数1:program;参数2:在片元着色器中用uniform修饰的变量名字的字符串。注意,该字符串必须同片元着色器中对应的变量名保持一致
    • 使用glUniform1i函数获取纹素,它也有两个参数,参数1:片元着色器中uniform的入口;参数2:纹理ID,默认为0。
    glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
    

    绘制

    开始绘制,存储到RenderBuffer,从RenderBuffer将图片显示到屏幕上。

    • 调用glDrawArrays函数,指定图元连接方式进行绘制
    • context调用presentRenderbuffer函数将绘制好的图片渲染到屏幕上显示
    glDrawArrays(GL_TRIANGLES, 0, 6);
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
    

    至此,使用GLSL加载纹理已经完成,完整代码见Demo地址

    从效果图上看到,图片呈倒立显示,这是因为OpenGL要求原点(0,0)位于图片的左下角,Y坐标从下往上增加,而图片纹理的原点(0,0)是位于图片的左上角,Y坐标从上往下增加。所以最后的照片呈上下倒置的效果。
    以下是几种解决方案:

    • 方案1:将顶点绕Y轴进行翻转。这样可以实现正常显示。
      问题:如何实现绕Y轴翻转
      解决:将顶点坐标与一个旋转矩阵相乘,得到的结果就是翻转之后的顶点坐标。
      重点:在3D课程中用的是横向量,在OpenGL ES用的是列向量。顶点坐标是一个1行4列的矩阵,因此,旋转矩阵必须是4行4列,这样相乘之后才能得到新的1行4列的顶点坐标。另外,要实现翻转,只需要将该方向的坐标数据进行反向,如当前需要沿X轴反向,只需要将X轴的数据全部*-1,即可将X轴的数据翻转。
      代码详见方案1代码
    • 方案2:可以解压缩图片的时候对图片进行翻转。
      解决:在context绘制的图片,对图片进行翻转。
      重点:由于翻转之后,顶点数据的坐标会发生变化,超过绘制的区域,因此在翻转之后需要将顶点移至绘制区域内。
      主要使用的函数有
    //先平移至合适的位置,也可以在翻转之后再移至绘制区域内
    CGContextTranslateCTM(context, 0, height);
    //将Y轴翻转
    CGContextScaleCTM(context, 1, -1);
    

    代码详见方案2代码

    • 方案3:修改片元着色器纹理坐标,将片元着色器中的纹理坐标在Y轴方向翻转。
      重点:如何获取纹理坐标的Y轴方向数据,通过'varyTextCoord.y'即可得到Y轴数据。将1.0-varyTextCoord.y即可实现翻转。
    vec2 newCoord = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
    gl_FragColor = texture2D(colorMap, newCoord);
    

    代码详见方案3代码

    • 方案4:修改顶点着色器纹理坐标,将顶点着色器的纹理坐标在Y轴方向翻转。
      该方案原理同方案3一样,只是在不同的着色器完成纹理坐标的翻转。
      代码详见方案4代码

    • 方案5:修改源顶点数据中顶点坐标和纹理坐标的映射关系。
      原理同方案3、4一致,只是直接在顶点数组中修改源数据。
      原顶点数据数组

    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,
    };
    

    修改后的顶点数组

    GLfloat attrArr[] ={
        0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
        -0.5f, -0.5f, -1.0f,    0.0f, 1.0f,
            
        0.5f, 0.5f, -1.0f,      1.0f, 0.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
        0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
    };
    

    代码详见方案5代码

    相关文章

      网友评论

          本文标题:OpenGL ES案例03 - 使用GLSL完成纹理图片加载

          本文链接:https://www.haomeiwen.com/subject/cgkzjktx.html