美文网首页OpenGL & Metal
GLSL应用-- 显示图形

GLSL应用-- 显示图形

作者: 黑眼豆豆_ | 来源:发表于2020-08-05 14:17 被阅读0次

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


    image.png

    Shader

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

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


      image.png
    • vshfsh只是让开发者能够分得清顶点着色器和片元着色器,其实它的本质是一段NSString
    • 不建议用 NSString来直接编写,否则后期会混乱
    • 不建议使用中文注释,可能会导致错误调试很难

    shaderv.vsh

    shaderv.vsh中的代码如下:

    //顶点向量
    attribute vec4 position;
    //纹理向量
    attribute vec2 textCoordinate;
    //纹理(内部传递)
    varying lowp vec2 varyTextCoord;
    
    void main()
    {
        varyTextCoord = textCoordinate;
        gl_Position = position;
    }
    
    • 在GLSL中语言中,我们最常用的有3种修饰类型:
      1.uniform修饰从外部(oc、swift)传递到顶点着色器、片元着色器的变量,类似于常量
      2.attribute的特点是只能传入到顶点着色器,也只能在顶点着色器使用,一般修饰顶点坐标、纹理坐标、颜色等。
      2.varying,当我们需要将数据从顶点着色器传入片元着色器到中,就使用varying,就相当于一个桥接,注意2个着色器的变量名要一致。
    • gl_Position是内建变量(GLSL创建 ,我们只需要使用),表示顶点着色器计算后的顶点结果,因为这里并没有发生仿射变换,所以直接传给gl_Position就可以。

    shaderf.fsh

    //定义使用高精度
    precision highp float;
    //纹理坐标(从顶点着色器传来)
    varying lowp vec2 varyTextCoord;
    //纹理数据(采样器)
    uniform sampler2D textCoordMap;
    void main(){
        //获取纹素
        gl_FragColor = texture2D(textCoordMap,varyTextCoord);
    }
    
    • precision highp float表示默认使用高精度,如果不设置这段代码,有可能因为精度过小而产生bug。
      • highp表示高精度
      • lowp表示低精度
    • sampler2D textCoordMap传递纹理数据,是一个采样器,其实我们拿到的是一个纹理ID。
    • texture2D(textCoordMap,varyTextCoord)的意义就是拿到纹素,而纹素就是纹理对应坐标点的颜色值。
      • textCoordMap表示纹理
      • varyTextCoord表示纹理坐标
      • gl_FragColor也是一个内建变量,是一个颜色值

    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;
    
    • CAEAGLLayer提供了一个可用的绘图表面,就是将我们图形绘制到CAEAGLLayer上
    • EAGLContext用来保存发生的状态
    • FrameBufferRenderBuffer,我们平常创建的纹理(Texture)深度缓冲区(Depth Buffer)模板缓冲区(Stencil Buffer)都是基于渲染缓冲区对象(Render Buffer Objects),而渲染缓冲区最终会附着到FrameBuffer上。
    • Program,我们写的shader都会附着到Program上

    开始

            //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"];
    
    • 编译着色器、附着到Program
    #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
        //链接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轴坐标取反,就可以达到翻转效果了。

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

    相关文章

      网友评论

        本文标题:GLSL应用-- 显示图形

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