美文网首页OpenGL ES
第十节—初探GLSL

第十节—初探GLSL

作者: L_Ares | 来源:发表于2020-09-21 22:48 被阅读0次

    本文为L_Ares个人写作,包括图片皆为个人亲自操作,如需转载请表明原文出处。

    之前的渲染一直都是用GLKit来帮助我们完成,那么如果不借助GLKit框架的话,想要实现渲染效果,我们就需要自己来进行渲染代码的编写。

    想要使用着色器进行渲染的话,前提条件就是一定要有2个基本的对象:着色器对象和程序对象,程序对象,关于这一个步骤,在链接中的文章已经说明了原因和他们的作用。

    在创建了两个基本对象,并获取链接之后,着色器对象就需要开始它的工作,这个工作一般包含了大概如下的步骤:

    1. 创建顶点着色器对象,创建片元着色器对象。

    2. 将源代码链接到每个着色器对象。

    3. 编译着色器对象。

    4. 创建程序对象。

    5. 将已经编译过的着色器对象和程序对象链接。

    6. 链接程序对象。

    在没有完成这些步骤之前,我们是很难直接将着色器里面的内容和我们Client中的内容进行交互的。

    一、渲染缓冲区

    渲染缓冲区英文名:RenderBuffer

    RenderBuffer是一个通过应用分配的2D缓冲区。RenderBuffer可以用来分配和存储颜色、深度、模版,也可以用过一个framebuffer的颜色、深度、模板的附件。RenderBuffer就类似于窗口系统提供的一个可绘制的表面。

    但是,RenderBuffer不能被拿来当作一个GL的纹理直接使用。

    RenderBuffer里面包含了深度缓冲区(DepthBuffer)、模版缓冲区
    (StencilBuffer)、纹理(Texture)。

    在GLKit的相关介绍中说过,显示在你屏幕上的图形,是在帧缓冲区(FrameBuffer)中被呈现上去的,只不过GLKit框架帮我们创建过了FrameBuffer。FrameBuffer在OpenGl ES是非常重要的组件,GLKit本身也是苹果基于OpenGL ES来进行的封装,所以在不使用GLKit之后,图形依然也是在FrameBuffer中完成设置后呈现到屏幕上的,但是这里的framebuffer因为不用GLKit框架了,没有GLKView了, 就要我们自己来创建。

    在OpenGL ES中,常称FrameBuffer对象为FBO。

    那么framebuffer又和这里要说的RenderBuffer是什么关系呢?

    直接点的说,FameBuffer是来管理RenderBuffer的。真正用来存储颜色、深度、模版值的是RenderBuffer,而FrameBuffer是他们的一个附着点。关系图如下1.1所示:

    1.1.png

    图中颜色本身就是可以当作纹理使用的,比如纯色纹理。深度则是在OpenGL中就说过,深度的大小会影响颜色缓冲区存储的颜色值,近存远删。

    二、简单的GLSL实现图片渲染的案例

    先说明一下.vsh.fsh文件的创建,其实就是empty文件的创建,大家应该都使用过了,我就直接贴图2.1了,记得把后缀名加上就行。

    2.1.png

    然后说一下这着色器文件中,最简单的,也是最必需要写的东西,因为最好不要在这两个文件中写注释,所以就单独拿出来解释一下。

    .vsh(顶点着色器代码)

    
    //顶点坐标
    attribute vec4 position;
    //纹理坐标
    attribute vec2 textCoordinate;
    //varying是一个标记,声明了这个变量是用来在vsh和fsh文件之间传递的变量
    //lowp是指这个二维向量的单位:GLFloat,它的精度
    //这个参数是存储纹理坐标的
    varying lowp vec2 varyTextCoord;
    
    void main ()
    {
        //把纹理坐标值赋值给传递变量,由传递变量将纹理坐标传输到片元着色器
        varyTextCoord = textCoordinate;
        //gl_Position是GLSL的内建变量,也就是GLSL已经创建好了的,用来保存顶点坐标的变量
        gl_Position = position;
    }
    
    

    .fsh(顶点着色器代码)

    //这个就是刚才从顶点着色器传过来的纹理坐标,注意这里最好直接复制过来,因为一个字母都不许差
    varying lowp vec2 varyTextCoord;
    //uniform属性 sampler2D代表的是声明纹理属性,就是说声明这个变量是纹理,他是以类似标识符的方式存储的
    //也就是说不是把你真的纹理放进来了,而是给纹理声明了一个身份ID,由ID去索引相应的纹理
    uniform sampler2D colorMap;
    
    void main ()
    {
        //内建变量gl_FragColor(纹理采样器,纹理坐标)
        //参数1 : 纹理的身份ID
        //参数2 : 纹理坐标。
        //内建函数会返回一个vec4类型的rgba值
        //它的作用就是读取纹素
        gl_FragColor = texture2D(colorMap, varyTextCoord);
    }
    
    
    

    这里的代码要用直接复制走的话,用的时候记得把中文注释都删除掉,尽量避免出现错误的情况。

    下面直接上代码,但是这次绘制的图片是翻转过来的,原因很简单,这次的代码没有做之前GLKit里面设置的OriginBottomLeft,所以纹理原点没有在左下角,而是和view的一样,在左上角。

    另外,这只是自定义View里面的内容,所以要显示出来记得要在viewcontroller里面把view加上去。

    //
    //  JDView.m
    //  04GLSL渲染图片
    //
    //  Created by EasonLi on 2020/9/20.
    //  Copyright © 2020 EasonLi. All rights reserved.
    //
    
    #import "JDView.h"
    #import <OpenGLES/ES2/gl.h>
    
    #define MY_ORIGIN self.frame.origin
    #define MY_SIZE self.frame.size
    
    @interface JDView ()
    
    //继承于CALayer。是在iOS上用于绘制OpenGL ES的图层类
    @property (nonatomic, strong) CAEAGLLayer *eaglLayer;
    
    //上下文
    @property (nonatomic,strong) EAGLContext *nContext;
    
    //渲染缓冲区
    @property (nonatomic,assign) GLuint nRenderBuffer;
    
    //帧缓冲区
    @property (nonatomic,assign) GLuint nFrameBuffer;
    
    //Program
    @property (nonatomic,assign) GLuint nProgram;
    
    @end
    
    
    
    @implementation JDView
    
    #pragma mark - 重绘View
    - (void)layoutSubviews
    {
        
        //创建图层
        [self createLayer];
        
        //创建图形的上下文
        [self createContext];
        
        //清空缓存区
        [self cleanUpBuffers];
        
    /********************************************/
        
        //这里要注意,必须是先有渲染缓存区,再有帧缓存区,因为renderbuffer才是真的缓存颜色,模版,深度的地方
        //frameBuffer是附着点!!!相当于只是管理着renderbuffer
        
        //设置渲染缓存区
        [self setUpRenderBuffer];
        
        //设置帧缓存区
        [self setUpFrameBuffer];
    /********************************************/
        
        //渲染并呈现
        [self rendLayer];
        
    }
    
    #pragma mark - 创建图层
    - (void)createLayer
    {
        
        //要重写layerClass,把JDView的图层强转成CAEAGLLayer类型,并赋值给eaglLayer
        self.eaglLayer = (CAEAGLLayer *)self.layer;
        
        //配置一下分辨率的缩放因子
        [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
        
        //设置layer绘制的描述属性
        //描述属性接收字典类型,这里设置了绘图表面显示之后,不保留其内容。(一般默认都是不保留,就是说下一次重新绘制)
        //以及颜色格式是RGBA8888
        self.eaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
        
    }
    
    //重写一下返回图层类的方法,宿主图层换成CALayer子类CAEAGLLayer
    + (Class)layerClass
    {
        return [CAEAGLLayer class];
    }
    
    #pragma mark - 设置图层上下文
    - (void)createContext
    {
        
        //初始化上下文,设置OpenGL ES的版本,因为需求不大,OpenGL ES2.0足够,可以用3.0
        EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
        
        //判断这个上下文是否创建成功
        if (!context) {
            NSLog(@"上下文创建失败");
            return;
        }
        
        //设置当前上下文
        if (![EAGLContext setCurrentContext:context]) {
            NSLog(@"设置当前上下文失败");
            return;
        }
        
        //将局部变量的context赋值给我们的属性
        self.nContext = context;
        
    }
    
    #pragma mark - 清空缓存区
    - (void)cleanUpBuffers
    {
        
        // Buffer(缓存区)分为renderBuffer(渲染缓存区)和frameBuffer(帧缓存区)两种。都要清空
        
        //清空renderBuffer
        glDeleteBuffers(1, &_nRenderBuffer);
        self.nRenderBuffer = 0;
        
        //清空frameBuffer
        glDeleteBuffers(1, &_nFrameBuffer);
        self.nFrameBuffer = 0;
         
    }
    
    #pragma mark - 申请并设置渲染缓冲区
    - (void)setUpRenderBuffer
    {
        
        //定义一个存储缓存区的ID的变量
        GLuint renderBufferID;
        
        //申请缓存区,并将其身份ID赋值
        glGenRenderbuffers(1, &renderBufferID);
        
        //将渲染缓存区的身份ID赋值给属性来保存
        self.nRenderBuffer = renderBufferID;
        
        //根据缓存区ID绑定缓存区的类型
        glBindRenderbuffer(GL_RENDERBUFFER, self.nRenderBuffer);
        
        //将刻绘制对象,也即是我们的CAEAGLLayer图层对象,绑定到RenderBuffer对象
        BOOL result = [self.nContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglLayer];
        
        if (!result) {
            NSLog(@"绘制图层和渲染缓存区绑定失败");
        }
        
    }
    
    #pragma mark - 申请并设置帧缓存区
    - (void)setUpFrameBuffer
    {
        
        //定义保存帧缓存区ID的对象
        GLuint frameBufferID;
        
        //申请帧缓存区并将身份ID赋值
        glGenFramebuffers(1, &frameBufferID);
        
        //将得到的frameBufferID赋值给属性
        self.nFrameBuffer = frameBufferID;
        
        //根据缓存区ID,把它的绑定到对应的缓存区类型
        glBindFramebuffer(GL_FRAMEBUFFER, self.nFrameBuffer);
        
        //把framebuffer和renderbuffer绑定在一起
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.nRenderBuffer);
        
    }
    
    #pragma mark - 渲染并呈现
    - (void)rendLayer
    {
        
        //绘制前一样要设置好清屏颜色,和OpenGL是一样的
        glClearColor(0.3f, 0.3f, 0.3f, 1.f);
        
        //绘制前一定要清空缓冲区
        glClear(GL_COLOR_BUFFER_BIT);
        
        //设置视口大小
        //先拿到mainScreen主屏的缩放因子
        CGFloat scale = [UIScreen mainScreen].scale;
        //设置视口
        glViewport(MY_ORIGIN.x * scale, MY_ORIGIN.y * scale, MY_SIZE.width * scale, MY_SIZE.height * scale);
        
        //加载着色器,链接program,使用program
        [self loadShaderAndLinkUseProgram];
        
        //设置顶点
        [self makeVertex];
        
        //处理纹理信息
        [self makeTextureInfo];
        
        //绘图
        glDrawArrays(GL_TRIANGLES, 0, 6);
        
        //将渲染缓冲区(RenderBuffer)上的内容渲染到屏幕上
        [self.nContext presentRenderbuffer:GL_RENDERBUFFER];
        
    }
    
    #pragma mark - 加载着色器,链接并使用程序program
    - (void)loadShaderAndLinkUseProgram
    {
        
        //读取顶点着色器和片元着色器的程序文件
        //拿到顶点着色器和片元着色器的程序路径
        NSString *vshFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
        NSString *fshFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
        
        //加载着色器文件,并创建最终的程序
        self.nProgram = [self loadVertex:vshFile Fragment:fshFile];
        
        //链接程序
        glLinkProgram(self.nProgram);
        
        //获取链接的状态
        GLint linkStatus;
        glGetProgramiv(self.nProgram, GL_LINK_STATUS, &linkStatus);
        //判断程序是否链接成功
        if (linkStatus == GL_FALSE) {
            //失败的话要拿取错误信息,存储在数组里面
            //定义错误信息数组GLChar类型数组,直接分配内存空间
            GLchar message[512];
            //参数:(1)程序 (2)错误信息的内存大小 (3)从哪里开始放 (4)错误信息放在哪里,直接写message一样,数组首地址
            glGetProgramInfoLog(self.nProgram, sizeof(message), 0, &message[0]);
            NSLog(@"程序链接失败,失败信息 : %@",[NSString stringWithUTF8String:message]);
            return;
        }
        
        //使用Program
        glUseProgram(self.nProgram);
        
    }
    
    #pragma mark - 处理顶点数据
    - (void)makeVertex
    {
        
        //设置顶点坐标数组
        GLfloat vertexArr[] = {
        
            0.5f,-0.5f,0.f,   1.f,0.f,
            -0.5f,0.5f,0.f,   0.f,1.f,
            -0.5f,-0.5f,0.f,  0.f,0.f,
            
            0.5f,0.5f,0.f,    1.f,1.f,
            -0.5f,0.5f,0.f,   0.f,1.f,
            0.5f,-0.5f,0.f,   1.f,0.f
        
        };
        
        //处理顶点信息
        //定义变量存储顶点缓存区ID
        GLuint vertexID;
        
        //申请顶点缓存区,并将ID赋值
        glGenBuffers(1, &vertexID);
        
        //绑定缓存区ID和对应的缓存区类型
        glBindBuffer(GL_ARRAY_BUFFER, vertexID);
        
        //将顶点数据从CPU拷贝到GPU中,也就是内存数据放入显存
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertexArr), vertexArr, GL_DYNAMIC_DRAW);
        
        //将顶点数据通过Program,传入到顶点着色器的position中,并返回一个属性变量的位置
        //第二个参数必须和顶点着色器中的顶点坐标属性字母完全一致
        GLuint position = glGetAttribLocation(self.nProgram, "position");
        
        //打开属性通道,并且以合适的格式传输从buffer中读取顶点数据
        glEnableVertexAttribArray(position);
        
        //设置顶点坐标读取方式
        glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
        
    }
    
    #pragma mark - 设置纹理信息
    - (void)makeTextureInfo
    {
        
        //将纹理坐标通过Program传入到顶点和片元着色器,同样的,字母名称必须和着色器中定义的变量完全一致
        GLuint textCoord = glGetAttribLocation(self.nProgram, "textCoordinate");
        
        //打开属性通道,传输纹理坐标
        glEnableVertexAttribArray(textCoord);
        
        //设置纹理坐标的读取方式
        glVertexAttribPointer(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
        
        //加载纹理
        [self loadTexture:@"image1"];
        
        //设置纹理采样器
        //参数:
        //(1). 第一个是得到纹理的ID索引的位置,因为纹理是不经常改变的,所以用Uniform通道
        //(2). 第几个纹理
        glUniform1i(glGetUniformLocation(self.nProgram, "colorMap"), 0);
        
    }
    
    #pragma mark - 加载着色器shader,并返回Program信息
    - (GLuint)loadVertex:(NSString *)vertexFile Fragment:(NSString *)fragmentFile
    {
        
        //定义两个临时的着色器变量
        GLuint vertextShader, fragmentShader;
        
        //创建程序
        GLuint program = glCreateProgram();
        
        //编译顶点着色器和片元着色器程序
        //参数:
        //(1). 编译完成后的着色器的内存地址
        //(2). 编译的是哪个着色器,也就是着色器的类型。
        //(3). 着色器文件的项目路径
        //编译顶点着色器
        [self compileShader:&vertextShader type:GL_VERTEX_SHADER file:vertexFile];
        //编译片元着色器
        [self compileShader:&fragmentShader type:GL_FRAGMENT_SHADER file:fragmentFile];
        
        //把着色器都附着或者说链接上程序
        //附着顶点着色器
        glAttachShader(program, vertextShader);
        //附着片元着色器
        glAttachShader(program, fragmentShader);
        
        //用完了这两个临时的着色器变量,也就是附着到程序上面了,就可以删除掉了
        //删除顶点着色器
        glDeleteShader(vertextShader);
        //删除片元着色器
        glDeleteShader(fragmentShader);
        
        return program;
        
    }
    
    //编译着色器
    - (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
    {
        
        //读取shader文件的路径
        NSString *shaderFile = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
        //因为glShaderSouce这个函数需要的是字符串类型的指针,所以这里转成C语言的字符串
        const GLchar *source = (GLchar *)[shaderFile UTF8String];
        
        //创建一个shader,并直接将创建的shader放入参数传过来的着色器内容(这里的*不是指的地址,是指的临时着色器的内容)
        *shader = glCreateShader(type);
        
        //将着色器源码附着到着色器对象上
        //参数:
        //(1). shader,要编译的着色器对象(*shader)
        //(2). 着色器源码字符串的数量,就是用了几个字符串写的或者说承载的着色器源码
        //(3). 真正的着色器程序的源码,也就是vsh和fsh里面的。(这就是第二个参数说的那一个字符串的地址)
        //(4). 着色器源码字符串的长度,如果不知道或者说不确定,写NULL,NULL代表字符串的终止位
        glShaderSource(*shader, 1, &source, NULL);
        
        //将着色器源码编译成目标代码
        glCompileShader(*shader);
        
    }
    
    #pragma mark - 从图片中加载纹理
    - (void)loadTexture:(NSString *)textureFile
    {
        
        //将UIImage类型的图片转换成CGImageRef,因为纹理最终需要的是像素位图,也就是要解压图片
        CGImageRef spriteImage = [UIImage imageNamed:textureFile].CGImage;
        
        //可以判断一下是否获得到了像素位图
        if (!spriteImage) {
            NSLog(@"解压缩图片失败 : %@",textureFile);
            //非正常运行程序导致程序退出。exit(0)是正常运行程序导致退出
            exit(1);
        }
        
        //成功拿到位图了,获取图片的宽高的大小
        size_t width = CGImageGetWidth(spriteImage);
        size_t height = CGImageGetHeight(spriteImage);
        
        //获取图片字节数是多少  也就是图片面积 * 颜色通道数量(RGBA就是4个)
        //也可以用malloc,malloc(width * height * 4 * sizeof(GLubyte));
        //稍提一嘴,calloc就是在内存的动态存储区上,分配第一个参数个数量的,每个单位长度为第二个参数的大小的连续空间
        //返回值是指向分配起始地址的指针,分配失败的话,返回值是NULL
        //calloc会清空分配的内存,而malloc不会。所以自行选择
        GLubyte *spriteByte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
        
        //创建上下文
        //参数:
        //(1). 指向要渲染的绘制图像的地址
        //(2). bitmap(位图)的宽,单位是像素
        //(3). bitmap(位图)的高,单位是像素
        //(4). bitsPerComponent是指内存中,像素的每个组件的位数,比如32位的RGBA,那么每一个颜色位都是8
        //(5). bytesPerRow指的是bitmap每一行内存需要多少bit(位)内存
        //(6). space指的是bitmap使用的颜色空间,可以通过CGImageGetColorSpace()获取
        //(7). bitmapInfo是枚举类型,CGImageAlpahInfo
        CGContextRef spriteContext = CGBitmapContextCreate(spriteByte, width, height, 8, width * 4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
        
        //在上下文上把图片绘制出来
        //定义变量,存储位图的尺寸CGRect
        CGRect rect = CGRectMake(0, 0, width, height);
        
        //使用默认的方法绘制
        CGContextDrawImage(spriteContext, rect, spriteImage);
        
        //绘制完成后就可以释放上下文了
        CGContextRelease(spriteContext);
        
        //绑定纹理到默认的纹理ID,因为glUniform里面也设置的0
        glBindTexture(GL_TEXTURE_2D, 0);
        
        //设置纹理属性,这里就不多说了,可以参考OpenGL的文章,里面有纹理的属性设置
        //参数:
        //(1). 纹理维度
        //(2). 要设置的纹理属性的名字
        //(3). 要设置的纹理属性的参数
        //这里要设置纹理过滤方式和环绕方式
        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);
        
        //要转一下图片宽高的类型,不然会提示,毕竟一个是unsigned的size_t,但是载入纹理要的是int_32
        float tWidth = width,tHeight = height;
        
        //载入2D纹理
        //https://www.jianshu.com/p/4e2bb76e31c3  这里有解释
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tWidth, tHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteByte);
        
        //图片数据也用完了,可以释放了
        free(spriteByte);
        
    }
    
    
    /*
    // Only override drawRect: if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    - (void)drawRect:(CGRect)rect {
        // Drawing code
    }
    */
    
    @end
    

    效果图如下2.2所示:

    2.2.png

    相关文章

      网友评论

        本文标题:第十节—初探GLSL

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