OpenGL之自定义着色器实现图片加载

859 阅读7分钟

在iOS开发中我们都知道使用UIImageView来加载图片,本文主要是通过自定义顶点与片元着色器来实现图片的加载。

主要步骤

  1. 设置CAEAGLLayer

  2. 设置上下文

  3. 清空缓冲区

  4. 设置帧缓存区

  5. 设置渲染缓冲区

  6. 顶点与片元着色器的编写

  7. 绘制图片

以上步骤中除了6以外均在自定义view中实现

工程设置

自定义一个ESVIew类继承于UIView

在VIewController的ViewDIdLoad方法中将self.view强转成ESView,必须注意的是如果你的ViewController是Main.storyborad的,则必须在storyborad中设置view继承于ESView,否则强转不会成功。

设置CAEAGLLayer

将self.layer设置成CAEAGLLayer,并重写类方法layerClass,代码如下

/// 返回CAEAGLLayer类
+(Class)layerClass {
    return [CAEAGLLayer class];
}
-(void)setUpLayer {
    self.myEaglLayer = (CAEAGLLayer *)self.layer;
    //设置分辨率
    [self setContentScaleFactor:[UIScreen mainScreen].scale];
    //设置描述属性
    self.myEaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
}

设置context

创建并设置当前view的上下文

-(void)setUpContext {
    self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    if (!self.myContext) {
        NSLog(@"创建上下文失败");
        return;
    }
    //设置上下文
    if (![EAGLContext setCurrentContext:self.myContext]) {
        NSLog(@"设置上下文失败");
        return;
    }
}

清空缓存区

清空帧缓存区以及渲染缓冲区

-(void)clearRenderBufferWithFramBuffer {
    glDeleteBuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
    glDeleteBuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
}

设置帧缓存

-(void)setRenderBuffer {
    //1.定义一个缓存区ID
    GLuint buffer;
    //2.申请一个缓存区标志
    glGenRenderbuffers(1, &buffer);
    //3.
    self.myColorRenderBuffer = buffer;

    //4.将标识符绑定到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);

    //5.将可绘制对象drawable object's  CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEaglLayer];
}

设置渲染缓存

-(void)setFrameBuffer {
    //1.定义一个缓存区ID
    GLuint buffer;
    //2.申请一个缓存区标志
    glGenBuffers(1, &buffer);
    //3.
    self.myColorFrameBuffer = buffer;

    //4.
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);

    /*生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
     调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
     */

    //5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}

绘制(重点+难点)

绘制是我们使用着色器显示图片的重点和难点,在绘制中我们主要分为以下几个步骤

  • 设置视图窗口

  • 根据顶点与片元着色器文件生成program并连接program以及检测是否连接成功

  • 设置图片的顶点与纹理坐标

  • 读取顶点着色器中position,将顶点数据从内存copy到显存用来绘制图片位置

  • 读取片元着色器的纹理信息,将纹理数据从内存copy到显存中用来绘制纹理图片

  • 加载图片,用来解析图片

  • 设置纹理采样,并通过三角线将图片绘制到屏幕上

设置窗口视图

glClearColor(1, 1, 0, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    //设置窗口大小
    CGFloat scale = [[UIScreen mainScreen] scale];
    CGRect frame = self.frame;
    //此处*scale是获取像素的大小
    glViewport(frame.origin.x, frame.origin.y, frame.size.width * scale, frame.size.height * scale);

生成program并连接program

//获取顶点着色器以及片元着色器路径
    NSString *renderPath = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *framePath = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];

    //设置program
    self.myProgram = [self loadShader:renderPath frag:framePath];

    //连接program
    glLinkProgram(self.myProgram);
    //连接状态
    GLint linkState;
    //获取连接状态
    glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkState);
    if (linkState == GL_FALSE) {
        GLchar error[512];
        glGetProgramInfoLog(self.myProgram, sizeof(error), 0, &error[0]);
        NSString *message = [NSString stringWithUTF8String:error];
        NSLog(@"连接失败了:%@\n",message);
        return;
    }
    //使用program
    glUseProgram(self.myProgram);

实现生成program方法

//加载buffer
-(GLuint)loadShader:(NSString *)vert frag:(NSString *)frag {
    GLuint renderShader,frameShader;
    GLuint program = glCreateProgram();
    [self compileShader:&renderShader bufferType:GL_VERTEX_SHADER path:vert];
    [self compileShader:&frameShader bufferType:GL_FRAGMENT_SHADER path:frag];

    //连接着色器
    glAttachShader(program, renderShader);
    glAttachShader(program, frameShader);
    //删除不用的着色器(释放空间)
    glDeleteShader(renderShader);
    glDeleteShader(frameShader);
    return program;

}
//编译着色器代码
-(void)compileShader:(GLuint *)shader bufferType:(GLenum)type path:(NSString *)file {
    //1.读取文件路径字符串
    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar* source = (GLchar *)[content UTF8String];

    //2.创建一个shader(根据type类型)
    *shader = glCreateShader(type);

    //3.将着色器源码附加到着色器对象上。
    //参数1:shader,要编译的着色器对象 *shader
    //参数2:numOfStrings,传递的源码字符串数量 1个
    //参数3:strings,着色器程序的源码(真正的着色器程序源码)
    //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
    glShaderSource(*shader, 1, &source,NULL);

    //4.把着色器源代码编译成目标代码
    glCompileShader(*shader);
}

设置顶点与纹理坐标

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,//右下
       };

顶点坐标处理

GLuint bufferId;
    //申请顶点缓冲区标识符
    glGenBuffers(1, &bufferId);
    //绑定顶点缓冲区
    glBindBuffer(GL_ARRAY_BUFFER, bufferId);
    //把顶点数据copy到显存
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);

    //通过program获得顶点着色器代码中的postion,注意postion必须与顶点着色器的一致
    GLuint postion = glGetAttribLocation(self.myProgram, "position");
    //读取数据
    glEnableVertexAttribArray(postion);
    glVertexAttribPointer(postion, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);

纹理坐标处理

//纹理数据
    //通过program获得片元着色器的,注意textCoordinate必须与片元着色器的一致
    GLuint textCoor = glGetAttribLocation(self.myProgram, "textCoordinate");
    glEnableVertexAttribArray(textCoor);
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float*)NULL + 3);

图片数据解析

-(void)loadImageWithImageName:(NSString *)imageName {
    CGImageRef refImage = [UIImage imageNamed:imageName].CGImage;
    if (!refImage) {
        NSLog(@"转换图片失败");
        return;
    }
    //获得图片的大小
    size_t width = CGImageGetWidth(refImage);
    size_t height = CGImageGetHeight(refImage);
    //获取图片的data
    GLubyte *data = (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 imageContextRef = CGBitmapContextCreate(data, width, height, 8, width * 4, CGImageGetColorSpace(refImage), kCGImageAlphaPremultipliedLast);

    //在CGContextRef上--> 将图片绘制出来
    /*
     CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
     CGContextDrawImage
     参数1:绘图上下文
     参数2:rect坐标
     参数3:绘制的图片
     */
    CGRect frame = CGRectMake(0, 0, width, height);
    //翻转图片
//    [self rateImage:imageContextRef frame:frame];
    //使用默认方式绘制
    CGContextDrawImage(imageContextRef, frame, refImage);
    //释放上下文
    CGContextRelease(imageContextRef);

    //绑定纹理
    glBindTexture(GL_TEXTURE_2D, 0);

    //设置纹理属性
    //设置线性过滤
    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);

    //载入纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (float)width, (float)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

    //释放data
    free(data);
}

设置纹理采样以及绘制图片到屏幕

//设置纹理采样
    glUniform1i(glGetUniformLocation(self.myProgram, "colorMap"), 0);
    //使用三角形绘图
    glDrawArrays(GL_TRIANGLES, 0, 6);
    //渲染到屏幕上
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];

顶点着色器代码编写

按照以上步骤我们生成一个空文件,代码实现如下

attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main()
{
//    varyTextCoord = vec2(textCoordinate.x, 1.0 - textCoordinate.y);
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

代码解释:position是处理顶点数据,textCoordinate处理的是纹理数据,varyTextCoord是将顶点着色器的纹理数据传输到片元着色器,如main中的varyTextCoord = textCoordinate;

片元着色器编写

创建文件步骤如顶点着色器文件,只是命名不一样,代码实现为

precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main()
{
    //lowp vec4 temp = texture2D(colorMap, varyTextCoord);
    //gl_FragColor = temp;

//    gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x, 1.0 - varyTextCoord.y));
    gl_FragColor = texture2D(colorMap, varyTextCoord);

}

代码解释:varyTextCoord保存了从顶点着色器传过来的纹理数据,注意这个varyTextCoord一定要与顶点着色器的一致;colorMap是用来纹理采样的。

顶点着色器与纹理着色器的最终结果如下

其中.fsh是片元着色器,.vsh是顶点着色器

实现效果

我们发现实现的图片是倒着的,那么我们怎么实现正立的图片呢?

图片翻转实现

方法一:将图片顶点旋转,纹理坐标不变

GLuint rotate = glGetUniformLocation(self.myProgram, "rotateMatrix");
    float radians = 180 * 3.14159f / 180.0f;
    float s = sin(radians);
    float c = cos(radians);

    GLfloat zRotation[16] = {
        c, -s, 0, 0,
        s, c, 0, 0,
        0, 0, 1.0, 0,
        0.0, 0, 0, 1.0
    };
    glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);

方法二:解析图片时将图片翻转

#pragma mark - 绘制图片时翻转
-(void)rateImage:(CGContextRef)imageRefContext frame:(CGRect)rect {
    CGContextTranslateCTM(imageRefContext, rect.origin.x, rect.origin.y);
    CGContextTranslateCTM(imageRefContext, 0, rect.size.height);
    CGContextScaleCTM(imageRefContext, 1.0, -1.0);
    CGContextTranslateCTM(imageRefContext, -rect.origin.x, -rect.origin.y);
}

方案三:修改顶点着色器

在顶点着色器中翻转顶点坐标

void main()
{
    varyTextCoord = vec2(textCoordinate.x, 1.0 - textCoordinate.y);
//    varyTextCoord = textCoordinate;
    gl_Position = position;
}

方法四:在片元着色器中翻转纹理(不推荐使用)

该方法不推荐使用,这个执行次数是根据图片的像素多少决定的,每个像素点都会执行一次

void main()
{
    //lowp vec4 temp = texture2D(colorMap, varyTextCoord);
    //gl_FragColor = temp;

    gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x, 1.0 - varyTextCoord.y));
//    gl_FragColor = texture2D(colorMap, varyTextCoord);

}

方法五:在设置顶点的时候翻转顶点坐标

//通过修改纹理坐标旋转图片(s不动,t取反加1)
    GLfloat attrArr[] =
    {
        0.5f, -0.5f, 0.0f,     1.0f, 1.0f,//右下
        -0.5f, 0.5f, 0.0f,     0.0f, 0.0f,//左上
        -0.5f, -0.5f, 0.0f,    0.0f, 1.0f,//左下

        0.5f, 0.5f, 0.0f,      1.0f, 0.0f,//右上
        -0.5f, 0.5f, 0.0f,     0.0f, 0.0f,//左上
        0.5f, -0.5f, 0.0f,     1.0f, 1.0f,//右下
    };

旋转后的效果