OpenGL & Metal

GLSL应用-- 显示图形

2020-08-05  本文已影响0人  黑眼豆豆_

首先,我们看效果,如图所示,我们在屏幕上展示一个图片。


image.png

Shader

在前面的文章,我们讲过,GLSL语言中,我们只能对顶点着色器片元着色器进行操作,所以我们只需要创建顶点着色器和片元着色器文件。

  1. commond + N ——> Empty ——>
image.png
  1. 创建顶点着色器文件(shaderv.vsh)和片元着色器文件(shaderf.fsh)


    image.png

shaderv.vsh

shaderv.vsh中的代码如下:

//顶点向量
attribute vec4 position;
//纹理向量
attribute vec2 textCoordinate;
//纹理(内部传递)
varying lowp vec2 varyTextCoord;

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

shaderf.fsh

//定义使用高精度
precision highp float;
//纹理坐标(从顶点着色器传来)
varying lowp vec2 varyTextCoord;
//纹理数据(采样器)
uniform sampler2D textCoordMap;
void main(){
    //获取纹素
    gl_FragColor = texture2D(textCoordMap,varyTextCoord);
}

OC函数(编译、调用Shader)

准备工作

我们创建几个变量

//在iOS和tvOS上绘制OpenGL ES内容的图层,继承自CALyayer
@property(nonatomic,strong)CAEAGLLayer *myLayer;
//EAGL上下文
@property(nonatomic,strong)EAGLContext *context;
//frameBuffer
@property(nonatomic,assign)GLuint myFrameBuffer;
//renderBuffer
@property(nonatomic,assign)GLuint myRenderBuffer;
//Program
@property(nonatomic,assign)GLuint myProgram;

开始

        //1 设置图层
        [self setupLayer];
        //2 设置上下文
        [self setupContext];
        //3.清空缓存区
        [self deleteRenderAndFrameBuffer];
        //4.设置RenderBuffer
        [self setupRenderBuffer];
        //5.设置setupFrameBuffer
        [self setupFrameBuffer];
        //6.开始绘制
        [self renderLayer];
流程图.png

setupLayer

// 1.设置图层
-(void)setupLayer{
    //1.创建特殊图层
    /*
     重写layerClass,将CCView返回的图层从CALayer替换成CAEAGLLayer
     */
    self.myLayer = (CAEAGLLayer *)self.layer;
    NSLog(@"%@",self.myLayer);
    
    //设置scale
    [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
    
    //3.设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
    /*
     kEAGLDrawablePropertyRetainedBacking  表示绘图表面显示后,是否保留其内容。
     kEAGLDrawablePropertyColorFormat
         可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
     
         kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
         kEAGLColorFormatRGB565:16位RGB的颜色,
         kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。


     */
//    self.myLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];
    self.myLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
}

这一步操作就是将layer转化成CAEAGLLayer,注意,我们还需要加下面这个代码,否则转化不了

//没有该方法,layer不会赋值给CAEAGLLayer
+(Class)layerClass{
    return [CAEAGLLayer class];
}

setupContext

// 2.设置上下文
-(void)setupContext{
    //设置上下文
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:(kEAGLRenderingAPIOpenGLES3)];
    //判断是否创建成功
    if (!context) {
        NSLog(@"创建失败");
        return;
    }
    //设置当前上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"设置当前上下文失败");
        return;
    }
    //把上下文设置全局
    self.context = context;
}

设置上下文的目的就是用来保存发生的状态

deleteRenderAndFrameBuffer

因为OpenGL会保存状态,所以每次创建前都要进行重置、初始化

// 3.清空缓冲区
-(void)deleteRenderAndFrameBuffer{
    /*
    buffer分为frame buffer 和 render buffer2个大类。
    其中frame buffer 相当于render buffer的管理者。
    frame buffer object即称FBO。
    render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
    */
    glDeleteRenderbuffers(1, &(_myRenderBuffer));
    self.myRenderBuffer = 0;
    
    glDeleteFramebuffers(1, &_myFrameBuffer);
    self.myFrameBuffer = 0;
}

setupRenderBuffer

//设置RenderBuffer
-(void)setupRenderBuffer{
    GLuint renderBuffer;
    //申请一个缓冲区标志
    glGenRenderbuffers(1, &renderBuffer);
    //绑定标识符到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
    //将可绘制对象drawable object's  CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myLayer];
    self.myRenderBuffer = renderBuffer;
}

针对renderbufferStorage这个方法,官方解释如下:

Attaches an EAGLDrawable as storage for the OpenGL ES renderbuffer object bound to <target>

实际上就是将contextlayerrenderbuffer的形式作绑定,就是之后的会绘制到CAEAGLLayer上。

setupFrameBuffer

//设置FrameBuffer
-(void)setupFrameBuffer{
    GLuint frameBuffer;
    //申请一个缓冲区标志
    glGenFramebuffers(1, &frameBuffer);
    //绑定标识符到GL_RENDERBUFFER
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    /*生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
     调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
     */
    self.myFrameBuffer = frameBuffer;
    
    //5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myRenderBuffer);
}

glFramebufferRenderbuffer就是讲RenderBuffer附着到FrameBuffer上

renderLayer

    //清理屏幕颜色
    glClearColor(0, 0, 0, 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);
    //读取顶点着色器、片元着色器
    NSString *vShader = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fShader = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
#pragma -mark loadShader
-(GLuint)loadVertexShader:(NSString *)vertext andFragShader:(NSString *)fragment{
    GLuint vShader,fShader;
    //创建program
    GLint program = glCreateProgram();
    
    //2.编译顶点着色程序、片元着色器程序
    //参数1:编译完存储的底层地址
    //参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
    //参数3:文件路径
    [self compileShader:&vShader withType:GL_VERTEX_SHADER andPath:vertext];
    [self compileShader:&fShader withType:GL_FRAGMENT_SHADER andPath:fragment];
    
    //把编译好的程序附着到shader(shader -> program)
    glAttachShader(program, vShader);
    glAttachShader(program, fShader);
    
    //删除shader  以免占用内存
    glDeleteShader(vShader);
    glDeleteShader(fShader);

    return program;
}

#pragma -mark compileShader
-(void)compileShader:(GLuint *)shader withType:(GLenum)type andPath:(NSString *)path{
    //1.读取文件路径字符串
    NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    //转化成c语言字符串
    const GLchar *source = [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, nil);
    
    //把着色器代码编译成目标代买
    glCompileShader(*shader);
}

      1. compileShader这个方法的shader需要传递指针,以存放编译好的shader
      2. 编译完成后要进行删除glDeleteShader,以便清空缓存,释放空间

    //链接Program
    glLinkProgram(self.myProgram);
    //查看链接是否成功
    GLint linkStatus;
    glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLchar message[512];
        glGetProgramInfoLog(self.myProgram, sizeof(message), 0, &message[0]);
        NSString *messageString = [NSString stringWithUTF8String:message];
        NSLog(@"Program Link Error:%@",messageString);
        return;
    }

    NSLog(@"链接成功");
    glUseProgram(self.myProgram);

      1. glLinkProgram链接程序
      2. glGetProgramiv可以查看链接状态,glGetProgramInfoLog打印错误日志
      3. 编译完成后要进行删除glUseProgram,使用程序

//6.设置顶点、纹理坐标
    //前3个是顶点坐标,后2个是纹理坐标
    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 verBuffer;
    //(2)申请一个缓存区标识符
    glGenBuffers(1, &verBuffer);
    //(3)将verBuffer绑定到GL_ARRAY_BUFFER标识符上
    glBindBuffer(GL_ARRAY_BUFFER, verBuffer);
    //(4)把顶点数据从CPU内存复制到GPU上(帧缓冲区)
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
    
    //8.将顶点数据通过myPrograme中的传递到顶点着色程序的position
    //1.glGetAttribLocation,用来获取vertex attribute的入口的.
    //2.告诉OpenGL ES,通过glEnableVertexAttribArray,
    //3.最后数据是通过glVertexAttribPointer传递过去的
    
    //读取顶点通道ID
    GLuint positon = glGetAttribLocation(self.myProgram, "position");
    //开启通道
    glEnableVertexAttribArray(positon);
    //(3).设置读取方式
    glVertexAttribPointer(positon, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, 0);
    
    //读取纹理通道ID
    GLuint textCoord = glGetAttribLocation(self.myProgram, "textCoordinate");
    //开启通道
    glEnableVertexAttribArray(textCoord);
    //(3).设置读取方式
    glVertexAttribPointer(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);

      1.glGetAttribLocation(self.myProgram, "position")是获取通道ID
      2.positiontextCoordinate要和vsh中的数据一模一样
      3.glVertexAttribPointer传递数据到顶点着色器

-(void)setupTexture:(NSString *)fileName{
    //将image转化成 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    //判断图片读取是否成功
    if (!spriteImage) {
        NSLog(@"读取失败");
        exit(1);
    }
    
    // 获取宽度
    CGFloat width = CGImageGetWidth(spriteImage);
    // 获取高度
    CGFloat height = CGImageGetHeight(spriteImage);
    
    //3.获取图片字节数 宽*高*4(RGBA)
    GLubyte * spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));

    //4.创建上下文
    /*
     参数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);
    
    //5、在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);
    
    //9.设置纹理属性
    /*
     参数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);

    //载入纹理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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    //释放spriteData
    free(spriteData);
}

glBindTexture(GL_TEXTURE_2D, 0)可以省略,因为当前只有一个纹理

    //设置纹理采样器   sampler2D
    glUniform1i(glGetUniformLocation(self.myProgram, "textCoordMap"), 0);
    //绘制
    glDrawArrays(GL_TRIANGLES, 0, 6);
    //从渲染缓冲区到屏幕上
    [self.context presentRenderbuffer:GL_RENDERBUFFER];

      1.glUniform1i(glGetUniformLocation(self.myProgram, "textCoordMap"), 0)就是将纹理ID传进去,跟textCoordMap进行绑定,因为此时纹理本身已经载入成功了。
      2.glGetUniformLocation是拿到shader中纹理的ID,因为textCoordMap是用uniform修饰,所以传递数据也要用uniform。
      3.[self.context presentRenderbuffer:GL_RENDERBUFFER]将context中的数据渲染到屏幕上。

最后效果,如图:


image.png

翻转策略

因为之前我们已经讲过,由于纹理坐标原点是左下角,而图片显示原点是左上角,所以不设置就会发生翻转

解决方法

    //设置顶点
    varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);

我们在顶点着色器中加这么一句话就可以了,其实原理也很简单,保持x轴坐标不变,y轴坐标取反,就可以达到翻转效果了。

当然,还有很多种解决办法,大家可以自己去尝试一下。

上一篇下一篇

猜你喜欢

热点阅读