美文网首页
OpenGLES(五)- ESLS案例:纹理贴图

OpenGLES(五)- ESLS案例:纹理贴图

作者: Henry________ | 来源:发表于2020-08-07 15:40 被阅读0次

    OpenGLES(五)- ESLS案例:纹理贴图

    阅读时间大概10-15分钟

    结果效果图

    首先梳理一下大体思路,下方代码也会按照这个顺序:

    1. EAGLLayer获取,设置layer图层
    2. content创建
    3. 清空缓存区(frameBuffer,renderBuffer)
    4. 设置renderBuffer
    5. 设置frameBuffer
    6. 手动编译、链接着色器程序
    7. 开始绘制 - 着色器创建
    
    全局属性定义
    //变量名基本就是解释
    @property(nonatomic,strong)CAEAGLLayer *myLayer;
    @property(nonatomic,strong)EAGLContext *myContent;
    @property(nonatomic,assign)GLuint myRenderBuffer;
    @property(nonatomic,assign)GLuint myFrameBuffer;
    @property(nonatomic,assign)GLuint myProgram;
    
    1. EAGLLayer获取,设置layer图层
    + (Class)layerClass{
        //1.需要重写view的子类方法,返回特定的layer,否则所有绘制动作是无效的
        return [CAEAGLLayer class];
    }
    -(void)setupLayout {
        //2.获取layer
        //view中存在一个特殊的图层,用于OpenGL的渲染
        self.myLayer = (CAEAGLLayer *)self.layer;
        
        //3.设置scale
        CGFloat scale = [[UIScreen mainScreen] scale];
        [self setContentScaleFactor:scale];
        
        //4.设置描述属性
        /*
          kEAGLDrawablePropertyRetainedBacking  表示绘图表面显示后,是否保留其内容。
          kEAGLDrawablePropertyColorFormat 可绘制表面的内部颜色缓存区格式。这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
         */
        self.myLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,
                                            kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
    }
    
    • 需要注意的是layerClass这个方法的重写
    2. content创建
    -(void)setupContent{
        //1. 创建上下文
        self.myContent = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
        if(!self.myContent){
            NSLog(@"create content failed");
            return;
        }
        
        //2.设置图形上下文
        if(![EAGLContext setCurrentContext:self.myContent]){
            NSLog(@"set Current Context failed");
            return;
        }
    }
    
    3.清空缓存区
    -(void)cleanBuffer {
        /*
        buffer分为frame buffer 和 render buffer2个大类。
        其中frame buffer 相当于render buffer的管理者。
        frame buffer object即称FBO。
        render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
        */
        
        //1. 清空渲染缓存区
        //该渲染缓存区被重置为0,被标记为未使用。与之连接的帧缓存区也被断开。
        glDeleteRenderbuffers(1, &_myRenderBuffer);
        
        //2. 清空帧缓存区
        //使用该函数和glDeleteFramebuffers效果相同,但是renderBuffer也可以使用
        glDeleteBuffers(1, &_myFrameBuffer);
    }
    
    在设置之前需要解释一下FrameBuffer和RenderBuffer

    FrameBuffer是RenderBuffer的管理者,两者共同组成了帧缓存区。FrameBuffer是没有存储功能的,具体的存储功能实际是RenderBuffer。

    图片来自简书-Style_月月
    • FrameBuffer上有3个附着点:

    颜色附着点(Color Attachment):管理纹理、颜色缓冲区
    深度附着点(depth Attachment):会影响颜色缓冲区,管理深度缓冲区(Depth Buffer)
    模板附着点(Stencil Attachment):管理模板缓冲区(Stencil Buffer)

    • RenderBuffer有3种缓存区

    深度缓存区(Depth Buffer):存储深度值等
    模板缓存区(Stencil Buffer):存储模板

    • 纹理缓存区( Texture mip Images)

    保存的是MipMap中当前深度的切片。所以需要深度附着点和颜色附着点共同协作。

    4. 设置renderBuffer
    -(void)setRenderBuffer{
        //1. 创建渲染缓冲区ID
        GLuint rBuffer;
        glGenRenderbuffers(1, &rBuffer);
        
        //2. 绑定缓冲区
        glBindRenderbuffer(GL_RENDERBUFFER, rBuffer);
        
        //3. 指定存储在 renderbuffer 中图像的宽高以及颜色格式(从myLayer中获取),并按照此规格为之分配存储空间
        [self.myContent renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myLayer];
        
        self.myRenderBuffer = rBuffer;
    }
    
    5. 设置frameBuffer(FBO)
    //1. 创建渲染缓冲区ID
        GLuint fBuffer;
        glGenFramebuffers(1, &fBuffer);
        
        //2. 绑定缓冲区
        glBindFramebuffer(GL_FRAMEBUFFER, fBuffer);
        
        
        /*3. 生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
         使用函数进将渲染缓存区绑定到d帧缓存区对应的颜色附着点上,后面的绘制才能起作用
        */
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myRenderBuffer);
    
    6.着色器创建
    • 手动编译着色器代码量还是有点多的但是思路依旧很清晰
      手动编译、链接着色器程序:
      1. 顶点、片元着色器ID创建
      2. 着色器文件读取
      3. 着色器文件附着到着色器上
      4. 着色器编译
      5. 程序ID创建
      6. 着色器附着到程序上
      7. 清理着色器内存
      8. 程序链接
        8.1 链接状态获取
      9. 使用program
    -(void)setupShader{
        //1. 读取着色器地址
        NSString *verFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
        NSString *framFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
        
        //2. 加载、编译着色器,编辑、链接程序对象
        self.myProgram = [self startShaderProgram:verFile fFile:framFile];
    }
    
    /// 着色器程序启动
    -(GLuint)startShaderProgram:(NSString *)vertex fFile:(NSString *)fragment{
        //定义2个零时着色器对象
        GLuint verSharder, fragSharder;
        //着色器编译
        [self compileShader:&verSharder type:GL_VERTEX_SHADER path:vertex];
        [self compileShader:&fragSharder type:GL_FRAGMENT_SHADER path:fragment];
        //程序编译
        GLuint program;
        program = [self compileProgram:verSharder frag:fragSharder];
        //程序链接
        [self linkProgram:program];
        return program;
    }
    
    /// 顶点着色器创建、编译
    -(void)compileShader:(GLuint *)shader type:(GLenum)type path:(NSString *)path{
        //1. 顶点、片元着色器ID创建
        *shader = glCreateShader(type);
        
        //2. 读取着色器文件
        NSString *source = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
        
        //转为c字符串
         const GLchar* cSource = [source UTF8String];
        
        //3.将着色器源码附加到着色器对象上。
        //参数1:shader,要编译的着色器对象 *shader
        //参数2:numOfStrings,传递的源码字符串数量 1个
        //参数3:strings,着色器程序的源码(真正的着色器程序源码)
        //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
        glShaderSource(*shader, 1, &cSource, NULL);
        
        //4.着色器编译
        glCompileShader(*shader);
    }
    
    /// 程序对象创建
    -(GLuint)compileProgram:(GLuint)vertexShader frag:(GLuint)fragShader {
        //5. 程序ID创建
        GLint program = glCreateProgram();
        
        //6. 着色器附着到程序上,创建最终的程序
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragShader);
        
        //7. 不会立即删除着色器,而是将着色器进行标记,等待着色器不在连接任何程序对象时,他的内存将会被释放。
        glDeleteShader(vertexShader);
        glDeleteShader(fragShader);
        return program;
    }
    
    
    /// 程序链接
    -(void)linkProgram:(GLuint)program {
        //8. 程序链接
        glLinkProgram(program);
        GLint linkStatus;
        //获取编译状态
        glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
        if (linkStatus == GL_FALSE) {
            GLchar info[512];
            glGetProgramInfoLog(program, sizeof(info), 0, &info[0]);
            NSString *message = [NSString stringWithUTF8String: info];
            NSLog(@"Program Link Error:%@",message);
            return;
        }
        NSLog(@"Program Link Success!");
        
        //9.使用program
        glUseProgram(program);
    }
    
    7.开始绘制
    -(void)setupTexture{
        //1. 设置清屏颜色,颜色缓存区
        glClearColor(0.3, 0.2, 0.7, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);
        
        //2.设置视口
        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);
        
        //3.设置顶点、纹理坐标
        //前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,
        };
        
        //4.-----创建顶点缓存区--------
        //4.1 创建顶点缓存区
        GLuint vertex;
        glGenBuffers(1, &vertex);
        //4.2 绑定顶点缓存区
        glBindBuffer(GL_ARRAY_BUFFER, vertex);
        //4.3 将数据从内存中读取到顶点缓存区中
        glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_STATIC_DRAW);
        
        //5.-----处理顶点数据--------
        //5.1 获取顶点着色器中限定符为:attribute的句柄
        //注意:第二参数字符串必须和顶点着色器中的输入变量名保持一致
        GLuint position = glGetAttribLocation(self.myProgram, "position");
        //5.2 允许该变量position读取顶点缓存区的数据
        glEnableVertexAttribArray(position);
        //5.3 设置positions通过何种方式从顶点缓存区中读取顶点数据
        glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 0);
        
        //6.-----处理纹理坐标数据--------
        //6.1 获取顶点着色器中限定符为:attribute的句柄
        GLuint texCoord = glGetAttribLocation(self.myProgram, "textureCoord");
        //6.2 允许该变量texCoord读取顶点缓存区的数据
        glEnableVertexAttribArray(texCoord);
        //6.3 设置texCoord通过何种方式从顶点缓存区中读取纹理数据
        glVertexAttribPointer(texCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
        
        //7. 加载纹理图片
        [self loadImage:@"cat"];
        
        //8.-----处理纹理数据--------
        //8.1 获取着色器中限定符为:uniform的句柄
        GLuint texture = glGetUniformLocation(self.myProgram, "textureMap");
        //8.2 设置texture读取帧缓存区中的对应纹理ID=0(参数2)的纹理
        glUniform1f(texture, 0);
        
        //9. 绘制
        glDrawArrays(GL_TRIANGLES, 0, 6);
        
        //10. 从渲染缓存区显示到屏幕上
        [self.myContent presentRenderbuffer:GL_RENDERBUFFER];
    }
    //加载纹理图片
    -(BOOL)loadImage:(NSString *)picName{
        //1.将UIImage转为CGImage
        CGImageRef ref = [UIImage imageNamed:picName].CGImage;
        //判断图片是否获取成功
        if (!ref) {
            NSLog(@"Failed to load image %@", picName);
            return NO;
        }
        
        //2.读取图片大小、颜色空间
        size_t width = CGImageGetWidth(ref);
        size_t height = CGImageGetHeight(ref);
        CGColorSpaceRef space = CGImageGetColorSpace(ref);
        
        //3. 初始化接收图片数据的变量
        GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
        
        //4.创建coreGraphics的上下文
        /*
        参数1:data,指向要渲染的绘制图像的内存地址
        参数2:width,bitmap的宽度,单位为像素
        参数3:height,bitmap的高度,单位为像素
        参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
        参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
        参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
        */
        CGContextRef contentRef = CGBitmapContextCreate(spriteData, width, height, 8, width*4, space, kCGImageAlphaPremultipliedLast);
        
        //5. 将CGImage在CGContextRef上绘制出来
        /*
        CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
        CGContextDrawImage
        参数1:绘图上下文
        参数2:rect坐标
        参数3:绘制的图片
        */
        CGContextDrawImage(contentRef, CGRectMake(0, 0, width, height), ref);
        
        //6. 绘制完毕后释放CG上下文
        CGContextRelease(contentRef);
        //以上步骤统称为图片解压缩
        
        //7. 激活纹理空间
        //OpenGL中纹理ID0默认打开,所以该方法可省略
        //glActiveTexture(0);
        
        //8. 绑定纹理ID
        glBindTexture(GL_TEXTURE_2D, 0);
        
        //9. 设置纹理ID的参数
        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);
        
        //10. 载入纹理到帧缓存区中,并对应纹理ID=0
        float fw = width, fh = height;
        /*
        参数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, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
        
        free(spriteData);
        return YES;
    }
    
    绘制完成后,会发现绘制完成后图片是倒的。因为:顶点坐标的原点是在左下角,而纹理坐标的原点是在左上角。目前有4种思路来解决:
    1. 在CoreGraphic解压缩图片时,旋转图片(最常使用的方案)
    2. 在顶点着色器中使用矩阵旋转、缩放变换
    3. 在顶点、片元着色器中将纹理Y地址进行1-Y的翻转操作
    4. 修改纹理坐标,使之翻转

    OpenGL ES中图片倒置解决方案

    案例源码Git

    相关文章

      网友评论

          本文标题:OpenGLES(五)- ESLS案例:纹理贴图

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