美文网首页OpenGL
使用GLSL编写自定义着色器加载图片

使用GLSL编写自定义着色器加载图片

作者: iOSer_jia | 来源:发表于2020-08-04 14:59 被阅读0次

    概述

    GLSL(OpenGL Shading Language)是一种用于OpenGL的高级过程着色语言,开发者可以使用GLSL自定义编写顶点着色器和片元着色器实现图形效果。本文将使用GLSL自定义着色器着iPhone设备上渲染一张图片。

    相关环境配置

    开发工具
    Xcode 11.4.1

    系统环境
    macOS Catalina 10.15.6 / iOS 13.4.1

    使用语言
    OC/C/GLSL

    着色器编写

    为了方便查看和维护,我们一般把着色器单独编写在一个独立的文件,我们新建一个shaderv.vsh文件编写顶点着色器源码,shaderf.fsh文件编写片元着色器源码。

    Xcode无法编译着色器源码,所以“vsh“和”fsh“只是作为标示区分顶点着色器和片元着色器,并无其他意义。
    着色器源码中不要出现中文,避免无法预知的错误

    shderv.vsh中:

    attribute vec4 position;
    attribute vec2 textCoordinate;
    varying lowp vec2 varyTextCoord;
    
    void main()
    {
        varyTextCoord = textCoordinate;
        gl_Position = position;
    }
    
    

    attribute vec4 position; // 声明一个名为position、类型为4分量浮点向量的attribute通道,用来传递顶点坐标数据
    attribute vec2 textCoordinate; // 声明一个名为textCoordinate、类型为2分量浮点向量的attribute通道,用来传递纹理坐标数据,由于片元着色器没有attribute通道,所以需要从顶点着色器中传入
    varying lowp vec2 varyTextCoord; // 声明一个名为varyTextCoord、 低精度(因为纹理坐标范围是0~1)、类型为2分量浮点向量的变量,这个变量作用是将纹理坐标传递到片元着色器中
    varyTextCoord = textCoordinate; // 将纹理坐标传递到片元着色器
    gl_Position = position; // 传递顶点坐标给OpenGL ES,gl_Position是OpenGL ES的内建变量,用来传递顶点数据。

    shderf.fsh中:

    precision highp float;
    varying lowp vec2 varyTextCoord;
    uniform sampler2D colorMap;
    
    void main()
    {
        lowp vec4 temp = texture2D(colorMap, varyTextCoord);
        gl_FragColor = temp;
    }
    

    precision highp float; 指定float为高精度
    varying vec2 varyTextCoord;纹理坐标,从顶点着色器中传入,参数名称和类型一定要和顶点着色器中的纹理坐标参数一模一样
    uniform sampler2D colorMap;纹理,通过uniform通道传入
    lowp vec4 temp = texture2D(colorMap, varyTextCoord);获取纹理对应坐标下的纹素,及纹理坐标对象像素点的颜色值,texture2D()是OpenGL ES的一个内建函数,用来获取纹素,他需要传入两个参数,第一个传递纹理,第二个传递纹理坐标,返回对应坐标的颜色值。
    gl_FragColor = temp;gl_Position一样,gl_FragColor时OpenGL ES的一个内建变量,用来放回颜色值。

    顶点着色器的执行次数和顶点数量有关,比如10个顶点那么顶点着色器会执行10次,而片元着色器的执行次数和像素点数量有关,绘制一个100*100的图片片元着色器会志晖行100000次,顶点着色器和片元着色器的执行都是在GPU中执行,所以执行效率会很高,但是模拟器中并没有着色器,执行的时候时CPU在模拟GPU的执行。

    Client代码

    客户端代码的大体执行流程为:

    1. 创建CAEAGLLayer
    2. 创建上下文
    3. 清空缓冲区
    4. 设置RenderBuffer
    5. 设置FrameBuffer
    6. 开始绘制

    首先创建一个ESView,并将它的layer指定CAEAGLLayer来展示绘制的结果

    ESView初始化

    ESView.m 中

    // 导入入相关头文件
    #import "ESView.h"
    #import <OpenGLES/ES3/gl.h>
    
    // 声明相关属性
    @interface ESView ()
    
    @property(nonatomic, strong) CAEAGLLayer *myEagLayer;
    @property(nonatomic, strong) EAGLContext *myContext;
        
    @property(nonatomic, assign) GLuint myColorRenderBuffer;
    @property(nonatomic, assign) GLuint myColorFrameBuffer;
    
    @property(nonatomic, assign) GLuint myPrograme;
    
    @end
    
    

    指定layer为CAEAGLLayer

    + (Class)layerClass {
        return CAEAGLLayer.class;
    }
    

    在layoutSubviews中开始绘制流程

    - (void)layoutSubviews {
        // 1. 创建CAEAGLLayer
        [self setupLayer];
        // 2. 创建上下文
        [self setupContext];
        // 3. 清空缓冲区
        [self deleteRenderAndFrameBuffer];
        // 4. 设置RenderBuffer
        [self setupRenderBuffer];
        // 5. 设置FrameBuffer
        [self setupFrameBuffer];
        // 6. 开始绘制
        [self renderLayer];
    }
    

    创建CAEAGLLayer、创建上下文、清空缓冲区

    前三步比较简单,直接上代码

        // 获取layer
        self.myEagLayer = (CAEAGLLayer *)self.layer;
        
        // 设置scale
        CGFloat scale = UIScreen.mainScreen.scale;
        [self setContentScaleFactor:scale];
        
        // 设置参数
        self.myEagLayer.drawableProperties = 
        @{kEAGLDrawablePropertyRetainedBacking: @false, 
        kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
    
    
    - (void)setupContext {
    
        EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
        
        if (!context) {
            NSLog(@"context create fail");
            return;
        }
        
        // 设置当前上下文为全局上下文
        if (![EAGLContext setCurrentContext:context]) {
            NSLog(@"set current context fail");
            return;
        }
        
        self.myContext = context;
    }
    
    - (void)deleteRenderAndFrameBuffer {
        glDeleteRenderbuffers(1, &_myColorRenderBuffer);
        self.myColorRenderBuffer = 0;
        
        glDeleteFramebuffers(1, &_myColorFrameBuffer);
        self.myColorFrameBuffer = 0;
    }
    

    设置RenderBuffer和FrameBuffer

    关于renderbuffer和framebuffer的关系,可以将FrameBuffer看作RenderBuffer的管理者,一个renderbuffer对象是通过应用分配的一个2D图像缓存区,它能够用来分配和存储颜色、深度或者模版值,也能够在一个framebuffer里作为颜色、深度、模版的附件,一个renderbuffer类似于窗口系统提供的可绘制的表面,比如pBuffer。而一个framebuffer对象(FBO)提供了许多附着点,他虽然有着缓冲区的字眼,但他更像一个容器,他可以保存可以进行渲染的对象,比如渲染缓冲区,采用这种方式,FBO能够在保存OpenGL
    管线的输出时将需要的状态和表面绑定在一起。

    设置渲染缓冲区

    - (void)setupRenderBuffer {
        
        // 申请一个缓冲区标示
        GLuint buffer;
        glGenRenderbuffers(1, &buffer);
        
        // 绑定buffer到GL_RENDERBUFFER
        glBindRenderbuffer(GL_RENDERBUFFER, buffer);
        
        self.myColorRenderBuffer = buffer;
        
        // 将layer的存储绑定到renderbuffer
        [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
        
    }
    

    设置帧缓冲区

    - (void)setupFrameBuffer {
        
        // 申请一个缓冲区标示
        GLuint buffer;
        glGenFramebuffers(1, &buffer);
    
        // 绑定
        glBindFramebuffer(GL_FRAMEBUFFER, buffer);
    
        self.myColorFrameBuffer = buffer;
    
        //将渲染缓冲区绑定到frameBuffer的GL_COLOR_ATTACHMENT0上
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorFrameBuffer);
        
        GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
        if (status == GL_FRAMEBUFFER_COMPLETE) {
            NSLog(@"framebuffer complete");
        } else {
            NSLog(@"not complete");
        }
    }
    

    开始绘制

    绘制主要流程如下:

    1. 设置清屏颜色
    2. 清空缓冲区
    3. 设置视口
    4. 加载顶点着色器和片元着色器
    5. 设置顶点坐标和纹理坐标
    6. 将顶点坐标从内存保存到顶点缓冲区
    7. 将顶点坐标和纹理坐标传递到着色器
    8. 加载纹理
    9. 将纹理数据传递到着色器
    10. 开始绘制(指定图元连接方式)
    11. 提交渲染

    具体代码如下

    - (void)renderLayer {
        
        // 设置清屏颜色
        glClearColor(1, 0, 0, 1.0);
        // 清空
        glClear(GL_COLOR_BUFFER_BIT);
        
        
        // 设置视口大小
        CGFloat scale = UIScreen.mainScreen.scale;
        CGRect rect = self.frame;
        glViewport(rect.origin.x*scale, rect.origin.y*scale, rect.size.width*scale, rect.size.height*scale);
        
        // 读取顶点着色器、片元着色器
        NSString *vertFile = [NSBundle.mainBundle pathForResource:@"shaderv" ofType:@"vsh"];
        NSString *fragFile = [NSBundle.mainBundle pathForResource:@"shaderf" ofType:@"fsh"];
        
        // 加载着色器生成program
        self.myPrograme = [self loadShaders:vertFile withFrag:fragFile];
        
        // 链接program
        glLinkProgram(self.myPrograme);
        
        // 获取链接状态
        GLint linkStatus;
        glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
        
        if (GL_FALSE == linkStatus) {
            // 获取错误信息
            GLchar message[512];
            glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
            NSString *msgStr = [NSString stringWithUTF8String:message];
            NSLog(@"link program failure: %@", msgStr);
            return;
        }
        
        // 使用program
        glUseProgram(self.myPrograme);
        
         //设置顶点
        GLfloat attArr[] = {
            -1.0, -0.5, 0.0,  0.0, 0.0,
            1.0, -0.5, 0.0,  1.0, 0.0,
            1.0, 0.5, 0.0,  1.0, 1.0,
            -1.0, 0.5, 0.0,  0.0, 1.0,
        };
        
        // 将顶点数据从内存保存到显存中
        // 申请一个缓冲区标示
        GLuint attBuffer;
        glGenBuffers(1, &attBuffer);
        // 绑定
        glBindBuffer(GL_ARRAY_BUFFER, attBuffer);
        // 传递到显存
        glBufferData(GL_ARRAY_BUFFER, sizeof(attArr), attArr, GL_DYNAMIC_DRAW);
        
        // 将数据传递到着色器
        
        // 获取position通道
        GLuint position = glGetAttribLocation(self.myPrograme, "position");
        //苹果默认关闭attribute通道,需要打开通道
        glEnableVertexAttribArray(position);
        // 将顶点坐标传递到顶点着色器
        glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
        
        // 传递纹理坐标到着色器
        GLuint textCoordinate = glGetAttribLocation(self.myPrograme, "textCoordinate");
        glEnableVertexAttribArray(textCoordinate);
        glVertexAttribPointer(textCoordinate, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL + 3);
        
        // 加载纹理
        [self setupTexture:@"benzema"];
        
        //获取颜色uniform通道
        GLuint colorMap = glGetUniformLocation(self.myPrograme, "colorMap");
        //设置纹理采样器
        // 0 标示默认
        glUniform1f(colorMap, 0);
        
        // 绘图
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
        
        // 从渲染缓冲区显示到屏幕上
        [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
    }
    

    加载着色器

    加载着色器的过程如下:

    1. 获取着色器源码
    2. 将着色器源码附着到着色器对象
    3. 编译着色器
    4. 将着色器连接到program
    5. 链接program
    6. 使用program

    具体代码如下:

    // 加载顶点着色器和片元着色器,返回program
    - (GLuint)loadShaders:(NSString *)vert withFrag:(NSString *)frag {
        
        // 定义两个临时着色器对象
        GLuint vertShader, fragShader;
        // 创建program
        GLuint program = glCreateProgram();
        
        // 编译着色器
        [self compileShader:&vertShader type:GL_VERTEX_SHADER file:vert];
        [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
        
        // 将着色器连接到program
        glAttachShader(program, vertShader);
        glAttachShader(program, fragShader);
        
        // 释放不需要的shader
        glDeleteShader(vertShader);
        glDeleteShader(fragShader);
        
        return program;
        
    }
    
    // 编译shader
    - (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file {
        NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
        const GLchar *source = (GLchar *)content.UTF8String;
        
        // 创建一个shader
        *shader = glCreateShader(type);
        
        //将着色器源码附着到shader
        //参数1:shader,要编译的着色器对象 *shader
        //参数2:numOfStrings,传递的源码字符串数量 1个
        //参数3:strings,着色器程序的源码(真正的着色器程序源码)
        //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
        glShaderSource(*shader, 1, &source, NULL);
        
        //编译着色器源码
        glCompileShader(*shader);
    }
    

    加载纹理

    加载纹理的过程为:

    1. 获取图片数据
    2. 创建一个纹理id
    3. 绑定纹理id
    4. 设置纹理属性
    5. 载入纹理2D数据到纹理缓冲区
    6. 释放图片数据

    而通过我所能拿到的是UIImage对象,我们可以使用CoreGraphics框架获得纹理数据,具体流程为

    1. 通过UIImage获得CGImageRef对象
    2. 创建上下文
    3. 创建一个大小为CGImageRef大小的内存空间
    4. 使用CGContextDrawImage重绘获得纹理数据
    5. 释放上下文

    具体代码如下:

    - (GLuint)setupTexture:(NSString *)fileName {
        
        // 将uiimage转化为cgimagref
        CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
        if(!spriteImage) {
            NSLog(@"failed to load image %@", fileName);
            exit(1);
        }
        
        // 使用CoreGraphics重绘获取位图数据
        // 读取图片的大小
        size_t width = CGImageGetWidth(spriteImage);
        size_t heith = CGImageGetHeight(spriteImage);
        
        // 创建空间存放数据
        GLubyte *spriteData = calloc(width*heith*4, sizeof(GLubyte));
        
        // 创建上下文
        CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, heith, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
        
        // 绘制图片
        CGRect rect = CGRectMake(0, 0, width, heith);
        CGContextDrawImage(spriteContext, rect, spriteImage);
        
        // 释放上下文
        CGContextRelease(spriteContext);
        
        // 绑定纹理到默认id
        glBindBuffer(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)heith, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
        
        // 释放纹理
        free(spriteData);
        
        return 0;
        
    }
    

    执行

    最终显示的结果为


    15962994997321.jpg

    可以看到图片虽然显示成功,但是是上下颠倒的,具体原因将在下一篇文章作出解释和解决。

    相关文章

      网友评论

        本文标题:使用GLSL编写自定义着色器加载图片

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