OpenGL ES 加载图片

上一篇文章我们介绍了如何用GLKit加载图片,为从OpenGL到OpenGL ES做一个过渡,并且介绍了EGL 和 EAGL。
今天这篇文章,就真正的进入到OpenGL ES ,也是以加载一张图片作为案例,后续会慢慢更新更多的内容。

OpenGL ES 跟GLKit 加载图片的区别

1. 加载纹理
GLKit中的GLKTextureLoader的作用是加载纹理图片,为方便开发者开发,于是对GLSL做了很大的优化,只需要一句代码就可以加载出纹理图片并生成GLKTextureInfo供开发者使用。
在OpenGL ES中,需要开发者自己从读取图片到利用CoreGraphics绘制图片再到设置线性过滤、环绕方式等等,需要一个比较长的流程。

2.顶点着色器/片元着色器
GLKBaseEffect的作用是执行顶点着色器和片元着色器的工作,便于开发者使用,用起来也非常方便。简单的几句代码便可以完成顶点着色器、片元着色器的工作。
在OpenGL ES中,需要开发者自己创建缓冲区、着色器、编译着色器等等繁杂的工作。

3.绘制
在GLKit当中,GLKViewDelegate提供了代理方法,只需要很简单的代码便可以完成图形的绘制。
而在OpenGL ES当中却需要开发者主动去将内容呈现在显示器上。

思维导图

OpenGL ES加载图片.png

接下来,我们跟随思维导图一步一步做。

创建顶点着色器

tips:着色器中的注释在使用时尽量去掉,免得出现不必要的错误

  1. 创建empty文件并命名为shaderv.vsh,命名规则无所谓,目的是为了让开发者自己能分辨清楚哪个文件的作用是什么。也可以命名为vertexSahder.a,vertexSahder.b等等等等,都可以。只要自己分得清即可。这里vsh也是vertexSahder的缩写。
    OpenGL ES 加载图片_第1张图片
    image.png
  2. 声明变量
//四维向量 顶点坐标
attribute vec4 position;
//二维向量 纹理坐标
attribute vec2 textCoordinate;
//低精度二维向量 纹理坐标 
/*
此处用varying 修饰,表示要通过这个变量,将纹理坐标传递给片元着色器
lowp表示低精度 
精度可分为highp/mediump/lowp 分别对应高/中/低

****************************************
此处声明变量的方式,以及变量名。
在片元着色器中,要同样声明一个一模一样的,才能完成纹理坐标的传递。
****************************************
*/
varying lowp vec2 varyTextCoord;
  1. main方法
void main(){
    //varying 修饰,将纹理坐标传递到片元着色器
    varyTextCoord = textCoordinate;
    //给内建变量赋值
    gl_Position = position;
}

创建片元着色器

tips:着色器中的注释在使用时尽量去掉,免得出现不必要的错误

  1. 同样创建一个empty文件,命名为shaderf.fsh。这里的fshfragmentShader的缩写。
  2. 变量声明
//纹理坐标
varying lowp vec2 varyTextCoord;
//纹理采样器
uniform sampler2D colorMap;
  1. main方法
void main(){
    /*
    texture2D(纹理采样器,纹理坐标)
    这个方法可以获取坐标对应的纹素
    
    gl_FragColor 是GLSL的内建变量,用来将纹理颜色添加到对应的像素点上
    */

    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

初始化

  1. 创建view


    OpenGL ES 加载图片_第2张图片
    image.png
  2. import
    #import
  3. 变量声明
@property(strong,nonatomic)CAEAGLLayer *eaglLayer;

@property(strong,nonatomic)EAGLContext *context;

@property(assign,nonatomic)GLuint program;

@property(assign,nonatomic)GLuint frameBuffer;

@property(assign,nonatomic)GLuint renderBuffer;

创建CAEAGLLayer

-(void)setupLayer{
    //创建特殊图层
    /*
     重写layerClass,将当前View返回的图层从CALayer替换成CAEAGLLayer
     */
    self.eaglLayer = (CAEAGLLayer *)self.layer;
    //设置缩放
    [self setContentScaleFactor:[UIScreen mainScreen].scale];
    /*
     kEAGLDrawablePropertyRetainedBacking :NO (告诉CoreAnimation不要试图保留任何以前绘制的图像留作以后重用)
     kEAGLDrawablePropertyColorFormat :kEAGLColorFormatRGBA8 (告诉CoreAnimation用8位来保存RGBA的值)
     也可以不设置。默认值就是这两个
     链接:https://www.jianshu.com/p/b3852409edbc
     */
    NSDictionary *options = @{kEAGLDrawablePropertyRetainedBacking:@(false),
                             kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
    
    self.eaglLayer.drawableProperties = options;
}
//重写layer
+(Class)layerClass{
    return [CAEAGLLayer class];
}

设置EAGLContext上下文

-(void)setupContext{
     //创建context
    self.context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (!_context) {
        NSLog(@"context创建失败");
        return;
    }
    //设置当期那context并判断是否设置成功
    if ([EAGLContext setCurrentContext:self.context]==false) {
        NSLog(@"设置当前context失败!");
        return;
    }  
}

清除缓冲区

这个其实也可以不用写

-(void)deleteBuffers{
    glDeleteBuffers(1, &_frameBuffer);
    _frameBuffer = 0;
    glDeleteBuffers(1, &_renderBuffer);
    _renderBuffer = 0 ;
}

创建RenderBuffer

-(void)setupRenderBuffer{
    //定义标识符ID
    GLuint bufferID;
    //glGenRenderbuffers申请标识符
    glGenRenderbuffers(1, &bufferID);
    
    self.renderBuffer = bufferID;
    //绑定缓冲区,注意此处为glBindRenderbuffer,不是glBindBuffer
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
    //将可绘制对象的存储绑定到OpenGL ES renderbuffer对象。
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglLayer];
    
}

创建FrameBuffer

-(void)setupFrameBuffer{
    //定义标识符ID
    GLuint bufferID;
    //glGenFramebuffers申请标识符
    glGenFramebuffers(1, &bufferID);
    self.frameBuffer = bufferID;
    //绑定缓冲区glBindFramebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    //生成帧缓冲区,把RenderBuffer跟FrameBuffer绑定到一起
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.renderBuffer);
}

绘制

-(void)draw{
    //设置背景色
    glClearColor(0.8, 0.8, 0.8, 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);
    //获取vsh/fsh路径
    NSString *vertexShaderPath = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fragmentShaderPath = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];
    
    NSLog(@"%@  ---  %@",vertexShaderPath,fragmentShaderPath);
    
    //创建shader
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    //创建program
    GLuint program = glCreateProgram();
    //读取vsh/fsh内容
    NSString *vertexContent = [NSString stringWithContentsOfFile:vertexShaderPath encoding:NSUTF8StringEncoding error:nil];
    NSString *fragmentContent = [NSString stringWithContentsOfFile:fragmentShaderPath encoding:NSUTF8StringEncoding error:nil];
    //NSString转C字符串
    const char *vertexSource = (GLchar *)[vertexContent UTF8String];
    const char *fragmentSource = (GLchar *)[fragmentContent UTF8String];
    //替换shader源码内容
    glShaderSource(vertexShader, 1, &vertexSource, NULL);
    glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
    //编译shader
    glCompileShader(vertexShader);
    glCompileShader(fragmentShader);
    //附着shader到program
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    //删除shader
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    
    self.program = program;
    
    //连接program
    glLinkProgram(self.program);
    //声明变量存储连接状态
    GLint linkStatus;
    //获取program连接状态
    glGetProgramiv(self.program, GL_LINK_STATUS, &linkStatus);
    //如果连接失败
    if (linkStatus == false) {
        NSLog(@"连接失败");
        char msg[1024];
        //获取programInfo 信息
        glGetProgramInfoLog(self.program, sizeof(msg), 0, &msg[0]);
        //char 转 NSString
        NSString *message = [NSString stringWithCString:msg encoding:NSUTF8StringEncoding];
        NSLog(@"%@",message);
        return;
    }
    NSLog(@"program 连接成功!");
    //使用program
    glUseProgram(self.program);
    
    //编辑顶点坐标数组
    GLfloat vertexData[] = {
        
        0.5, -0.25, 0.0f,    1.0f, 0.0f, //右下
        0.5, 0.25, -0.0f,    1.0f, 1.0f, //右上
        -0.5, 0.25, 0.0f,    0.0f, 1.0f, //左上
        
        0.5, -0.25, 0.0f,    1.0f, 0.0f, //右下
        -0.5, 0.25, 0.0f,    0.0f, 1.0f, //左上
        -0.5, -0.25, 0.0f,   0.0f, 0.0f, //左下
    };
    
    //定义标识符
    GLuint bufferID;
    //申请标识符
    glGenBuffers(1, &bufferID);
    //绑定缓冲区
    glBindBuffer(GL_ARRAY_BUFFER, bufferID);
    //将顶点数组的数据copy到顶点缓冲区中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
    
    
    //从program中获取position 顶点属性
    GLuint position = glGetAttribLocation(self.program, "position");
    //开启顶点属性通道
    glEnableVertexAttribArray(position);
    //设置顶点读取方式
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *) NULL + 0);
    
    //从program中获取textCoordinate 纹理属性
    GLuint textCoordinate = glGetAttribLocation(self.program, "textCoordinate");
    //开启纹理属性通道
    glEnableVertexAttribArray(textCoordinate);
    //设置纹理读取方式
    glVertexAttribPointer(textCoordinate, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *) NULL + 3);
    
    //获取纹理图片
    CGImageRef cgImgRef = [UIImage imageNamed:@"test"].CGImage;
    if (!cgImgRef) {
        NSLog(@"纹理获取失败");
    }
    //获取图片长、宽
    size_t width = CGImageGetWidth(cgImgRef);
    size_t height = CGImageGetHeight(cgImgRef);
    
    //计算图片所占字节数 长 * 宽 * RGBA占4个字节
    GLubyte *byte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
    
    //
    //创建CGContextRef画布
    /*
     参数1:data,指向要渲染的绘制图像的内存地址
     参数2:width,bitmap的宽度,单位为像素
     参数3:height,bitmap的高度,单位为像素
     参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
     参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
     参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
     */
    CGContextRef contextRef = CGBitmapContextCreate(byte, width, height, 8, width * 4, CGImageGetColorSpace(cgImgRef), kCGImageAlphaPremultipliedLast);
    //长宽转成float 方便下面方法使用
    float w = width;
    float h = height;
    
    //绘制图片的位置
    CGRect rect = CGRectMake(0, 0, w, h);
    //在CGContextRef上--> 将图片绘制出来
    /*
     CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,CoreGraphics框架的原点在屏幕的左下角。
     参数1:绘图上下文
     参数2:rect坐标
     参数3:绘制的图片
     */
    CGContextDrawImage(contextRef, rect, cgImgRef);
    //图片绘制完成后,contextRef就没用了,释放
    CGContextRelease(contextRef);
    
    //0 代表第0个纹理 对应采样器的0
    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);
    
    //载入纹理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, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, byte);
    
    //释放byte
    free(byte);
    
    //设置纹理采样器,这里的 0 对应 glBindTexture的 0
    glUniform1i(glGetUniformLocation(self.program, "colorMap"), 0);
    //绘图
    glDrawArrays(GL_TRIANGLES, 0, 6);
    //将渲染缓冲区 呈现到 屏幕上
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

调用上述方法

-(void)layoutSubviews{
    [super layoutSubviews];
    //设置layer
    [self setupLayer];
    //设置context
    [self setupContext];
    //清除缓存区
    [self deleteBuffers];
    //设置渲染缓冲区
    [self setupRenderBuffer];
    //设置帧缓冲区
    [self setupFrameBuffer];
    //绘制
    [self draw];
}

最后在ViewController中创建OpenGLESView

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.glesView = [[OpenGLESView alloc]initWithFrame:self.view.bounds];
    self.view = self.glesView;
}

效果图

OpenGL ES 加载图片_第3张图片
image.png

看了效果图之后,聪明的你肯定发现了,这是一张macOS mojave的壁纸截图,而且图片倒过来了,图片本身是正的。


OpenGL ES 加载图片_第4张图片
image.png

于是这就引出了另外一个问题 ——OpenGL 纹理翻转

以下内容摘取自CC

关于纹理翻转

纹理翻转的原因是因为OpenGL要求纹理坐标原点(0,0)在图片左下角。
而图片信息中的原点(0,0)一般都在左上角,一行行绘制出来,就导致了图片的上下翻转。


iOS纹理翻转解决策略

第1种: 旋转矩阵翻转图形,不翻转纹理

让图形顶点坐标旋转180°, 而纹理保持原状。

    GLuint rotate = glGetUniformLocation(self.myPrograme, "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]);

第2种: 解压图片时,将图片源文件翻转

CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;

size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));

CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
  
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);

CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(spriteContext, 0, rect.size.height);
CGContextScaleCTM(spriteContext, 1.0, -1.0);
CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
CGContextDrawImage(spriteContext, rect, spriteImage); 

CGContextRelease(spriteContext);
glBindTexture(GL_TEXTURE_2D, 0);

第3种: 修改片元着色器,纹理坐标

varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
    gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));
}

第4种: 修改顶点着色器,纹理坐标

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

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

第5种:直接从源纹理坐标数据修改

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

翻转过后的效果图,我用了第三种方法

OpenGL ES 加载图片_第5张图片
image.png

那么这篇文章就到此为止了,感谢阅读。 ^ _ ^

你可能感兴趣的:(OpenGL ES 加载图片)