美文网首页
OpenGL ES加载图片

OpenGL ES加载图片

作者: AndyGF | 来源:发表于2020-08-05 07:43 被阅读0次

    OpenGL ES 加载图片, 本文使用 GLSL 编写顶点着色器和片元着色器, 由于 Xcode 不支持 GLSL 语言的编译和链接, 所以需要自己手动去做这些事情.

    原图如下, 四角上的坐标是我自己截图时加上去的, 对应的纹理坐标,

    原图.png

    注意: 本文只是示例 demo, 如需用到项目中, 需要自己进行对应的优化.

    主要分为以下6个关键步骤:

    以下几个都是自定义函数

    1. 设置图层
      setupLayer()
    2. 设置图形上下文
      setupContext()
    3. 清空缓存区
      deleteRenderAndFrameBuffer()
    4. 设置RenderBuffer
      setupRenderBuffer()
    5. 设置FrameBuffer
      setupFrameBuffer()
    6. 开始绘制
      renderLayer()

    初始化

    import UIKit
    import OpenGLES
    
    class GFShaderView: UIView {
    
        //在iOS和tvOS上绘制OpenGL ES内容的图层,继承于 CALayer
        private var myEaglLayer: CAEAGLLayer?
        private var myContext: EAGLContext?
        private var myColorRenderBuffer: GLuint = 0
        private var myColorFrameBuffer: GLuint = 0
        private var myPrograme: GLuint = 0
        
        override init(frame: CGRect) {
            super.init(frame: frame)
        
            //1.设置图层
            setupLayer()
            
            //2.设置图形上下文
            setupContext()
            
            //3.清空缓存区
            deleteRenderAndFrameBuffer()
    
            //4.设置RenderBuffer
            setupRenderBuffer()
            
            //5.设置FrameBuffer
            setupFrameBuffer()
            
            //6.开始绘制
            renderLayer()
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override class var layerClass: AnyClass {
            get {
                return CAEAGLLayer.self
            }
        }
        
        deinit {
            // 删除两个缓冲区
            glDeleteBuffers(1, &myColorFrameBuffer)
            glDeleteBuffers(1, &myColorRenderBuffer)
        }
    }
    
    1.设置图层
    extension GFShaderView {
        
        private func setupLayer() {
            
            // 1.创建特殊图层
            // 重写layerClass,将返回的图层 CALayer 替换成 CAEAGLLayer
            myEaglLayer = layer as! CAEAGLLayer
            
            // 2.设置scale
            contentScaleFactor = UIScreen.main.scale
            
            // 3.设置描述属性,这里设置不维持渲染内容以, 颜色格式为 RGBA8
            myEaglLayer?.drawableProperties = [
                kEAGLDrawablePropertyRetainedBacking: false,
                kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
            ]
        }
    }
    
    2.设置图形上下文
    extension GFShaderView {
        
        private func setupContext() {
            
            // 设置版本
            let api = EAGLRenderingAPI.openGLES3
            // 2.创建上下文
            guard let context = EAGLContext(api: api) else {
                print("Create context failed!")
                return
            }
            
            if !EAGLContext.setCurrent(context) {
                print("setCurrentContext failed!");
                return
            }
            // 4.赋值
            myContext = context
        }
    }
    
    3.清空缓存区
    extension GFShaderView {
        
        private func deleteRenderAndFrameBuffer() {
            
            /*
            buffer分为frame buffer 和 render buffer 2个大类。
            其中frame buffer 相当于render buffer的管理者。
            frame buffer object即称FBO。
            render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
            */
    
            glDeleteBuffers(1, &myColorRenderBuffer)
            glDeleteBuffers(1, &myColorFrameBuffer)
            
            myColorFrameBuffer = 0
            myColorRenderBuffer = 0
        }
    }
    
    4.设置RenderBuffer
    extension GFShaderView {
        
        private func setupRenderBuffer() {
            
            // 1.定义一个缓存区ID
            var buffer: GLuint = 0
            
            // 2.申请一个标识符
            glGenRenderbuffers(1, &buffer)
            
            // 3.赋值
            myColorRenderBuffer = buffer
            
            // 4.将 render buffer 标识符绑定到 render buffer 对象
            glBindRenderbuffer(GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
            
            // 5.将可绘制对象 CAEAGLLayer 的存储绑定到 OpenGL ES renderBuffer 对象
            myContext?.renderbufferStorage(Int(GL_RENDERBUFFER), from: myEaglLayer)
        }
        
    }
    
    5.设置FrameBuffer
    extension GFShaderView {
        
        private func setupFrameBuffer() {
            
            // 1.定义一个缓存区ID
            var buffer: GLuint = 0
            
            // 2.申请一个标识符
            glGenRenderbuffers(1, &buffer)
            
            // 3.赋值
            myColorFrameBuffer = buffer
            
            // 4.将 render buffer 标识符绑定到 render buffer 对象
            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), myColorFrameBuffer)
            
            /*
             生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
             调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
            */
            
            // 5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0 上。
            glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
        }
    }
    
    6.开始绘制

    主要包括 OpenGL ES 上下文准备, 创建 programe, 加载着色器, 链接着色器, 处理顶点和纹理坐标, 加载纹理数据, 渲染绘制.

    extension GFShaderView {
        
        private func renderLayer() {
            
            // 1.设置 背景色 / 缓冲区
            // 清屏颜色
            glClearColor(0.5, 0.5, 0.5, 1.0)
            // 清空缓冲区
            glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
            
            // 2.设置视口大小
            let scale = UIScreen.main.scale
            let vx = GLint(frame.origin.x * scale)
            let vy = GLint(frame.origin.y * scale)
            let vw = GLsizei(frame.size.width * scale)
            let vh = GLsizei(frame.size.height * scale)
            glViewport(vx, vy, vw, vh)
            
            // 3.读取顶点着色程序、片元着色程序
            let vertFile = Bundle.main.path(forResource: "ShaderV", ofType: ".vsh") ?? ""
            let fragFile = Bundle.main.path(forResource: "ShaderF", ofType: ".fsh") ?? ""
            
            myPrograme = glCreateProgram()
            
            // 4.加载 shader 程序 并绑定到 myPrograme 上
            loadShaders(vertexShader: vertFile, fragmentShader: fragFile)
            
            // 5.链接
            glLinkProgram(myPrograme)
            
            var linkStatus: GLint = -1
            glGetProgramiv(myPrograme, GLenum(GL_LINK_STATUS), &linkStatus)
            
            if linkStatus == GL_FALSE {
                let message = UnsafeMutablePointer<GLchar>.allocate(capacity: 512)
                let length = GLsizei(MemoryLayout<GLchar>.size * 512)
                glGetProgramInfoLog(myPrograme, length, nil, message)
                let messageString = String(utf8String: message) ?? ""
                print("Program Link Error: \(messageString)")
                return;
            }
            
            print("Program Link Success!")
            
            // 6.使用 myPrograme
            glUseProgram(myPrograme)
            
            // 7.设置顶点 / 纹理坐标 (30 个元素)
            let attrArr: [GLfloat] = [
                0.5,  -0.5, -1.0,    1.0, 0.0,
                -0.5, 0.5,  -1.0,    0.0, 1.0, //左上
                -0.5, -0.5, -1.0,    0.0, 0.0, //左下
    
                0.5,  0.5, -1.0,     1.0, 1.0, //右上
                -0.5, 0.5, -1.0,     0.0, 1.0,
                0.5, -0.5, -1.0,     1.0, 0.0, //右下
            ]
    
            // 8.处理顶点数据 和 纹理坐标数据
            var attrBuffer: GLuint = 0
            // 申请一个缓存区标识符
            glGenBuffers(1, &attrBuffer)
            // 将 attrBuffer 绑定到 GL_ARRAY_BUFFER 标识符上
            glBindBuffer(GLenum(GL_ARRAY_BUFFER), attrBuffer)
            // 顶点数据 CPU 内存 copy 到 GPU 显存
            glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * 30, attrArr, GLenum(GL_DYNAMIC_DRAW))
            
            // 9.将顶点数据通过 myPrograme 中的传递到顶点着色程序的 position
            let fSize = MemoryLayout<GLfloat>.size
            let position = glGetAttribLocation(myPrograme, "position")
            glEnableVertexAttribArray(GLuint(position))
            glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(fSize * 5), UnsafeRawPointer(bitPattern: 0))
            
            // 10.将纹理数据通过 myPrograme 中的传递到顶点着色程序的 textCoordinate
            let textCoor = glGetAttribLocation(myPrograme, "textCoordinate")
            glEnableVertexAttribArray(GLuint(textCoor))
            glVertexAttribPointer(GLuint(textCoor), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(fSize * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 3))
            
            // 11.加载纹理
            loadTexture("kunkun")
            
            // 12.设置纹理采样器
            let lc = glGetUniformLocation(myPrograme, "colorMap")
            glUniform1i(lc, 0)
            
            // 13.绘图
            glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
            
            // 14.从渲染缓存区显示到屏幕上
            myContext?.presentRenderbuffer(Int(GL_RENDERBUFFER))
        }
    }
    
    6.1加载 shader 程序

    加载着色器代 > 编译 > 绑定到 myPrograme

    // MARK: ---------- shader -----------
    extension GFShaderView {
        
        /// 加载 shader
        private func loadShaders(vertexShader vFile: String, fragmentShader fFile: String) {
            
            // 1.定义两个着色器
            var verShader: GLuint = 0
            var fragShader: GLuint = 0
            
            // 2.编译顶点着色器 / 片元着色器
            compileShader(shader: &verShader, type: GLenum(GL_VERTEX_SHADER), file: vFile)
            compileShader(shader: &fragShader, type: GLenum(GL_FRAGMENT_SHADER), file: fFile)
            
            // 3.创建最终的程序
            glAttachShader(myPrograme, verShader)
            glAttachShader(myPrograme, fragShader)
            
            // 4.释放不需要的 shader
            glDeleteShader(verShader)
            glDeleteShader(fragShader)
        }
        
        /// 编译 shader
        /// - Parameters:
        ///   - shader: 着色器
        ///   - type: 着色器类型
        ///   - file: 着色器代码文件的字符串
        private func compileShader(shader: inout GLuint, type: GLenum, file: String) {
            
            // 1.获取着色器程序源码文件中字符串
            guard let content = try? String(contentsOfFile: file) else {
                print("Get shader \(type) file failed")
                return
            }
            
            // 2.创建一个 shader
            shader = glCreateShader(type)
    
            content.withCString {
                
                var pointer: UnsafePointer<GLchar>? = $0
                
                // 3.将源码附加到着色器上
                //参数1:shader,要编译的着色器对象 *shader
                //参数2:count,传递的源码字符串数量 1个
                //参数3:string,着色器程序的源码(真正的着色器程序源码)
                //参数4:length,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
                glShaderSource(shader, 1, &pointer, nil)
            }
            
            // 4.把着色器代码编译成目标代码
            glCompileShader(shader)
        }
    }
    
    6.2.加载图片纹理
     extension GFShaderView {
        /// 从资源图片中加载纹理
        /// - Parameter fileName: 图片名称
        private func loadTexture(_ fileName: String) {
            
            // 1.将 UIImage 转换为 CGImage
            guard let spriteImage = UIImage(named: fileName)?.cgImage,
                let space = spriteImage.colorSpace else {
                print("Failed to load image \(fileName)")
                exit(1)
            }
            
            // 2.读取图片的大小,宽和高
            let spWidth = spriteImage.width
            let spHeight = spriteImage.height
            
            // 3.获取图片字节数 宽*高*4(RGBA)
            let byteSize = MemoryLayout<GLubyte>.size
            let spriteData = UnsafeMutablePointer<GLubyte>.allocate(capacity: byteSize * spWidth * spHeight * 4)
            
            // 4.创建上下文CGImageAlphaPremultipliedLast
            let spriteContext = CGContext(data: spriteData, width: spWidth, height: spHeight, bitsPerComponent: 8, bytesPerRow: spWidth * 4, space: space, bitmapInfo: 1)
            
            // 5.在CGContextRef上--> 将图片绘制出来
            let rect = CGRect(x: 0, y: 0, width: spWidth, height: spHeight)
            
           // 6.使用默认方式绘制
            spriteContext?.draw(spriteImage, in: rect)
            
            // 7.绘制完成
            UIGraphicsEndImageContext()
            
            // 8.绑定纹理到默认的纹理ID
            glBindTexture(GLenum(GL_TEXTURE_2D), 0)
            
            // 9.设置纹理属性
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
            
            // 10.载入纹理 2D 数据
            glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(spWidth), GLsizei(spHeight), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), spriteData)
            
            // 11.释放 spriteData
            free(spriteData)
        }
    }
    
    7.顶点着色器
    attribute vec4 position;
    attribute vec2 textCoordinate;
    varying lowp vec2 varyTextCoord;
    
    void main() {
        varyTextCoord = textCoordinate;
        gl_Position = position;
    }
    
    8.片元着色器
    precision highp float;
    varying lowp vec2 varyTextCoord;
    uniform sampler2D colorMap;
    
    void main() {
        gl_FragColor = texture2D(colorMap, varyTextCoord);
    }
    

    执行起来的效果

    程序渲染出来的样子


    程序渲染出来的样子.png

    我们想像中的样子


    我们想像中的样子.png

    如下图, 此时我们发现图片是倒置的, 为什么呢, 因为 OpenGL ES 和 纹理坐标系 y 轴正方向都向上, 而我们手机屏幕 y 轴 的正方向向下, 所以在就产生了上面的效果, 解决办法很简单, 也有很多种, 不同情况可以不同对待, 我采用调整纹理坐标对应顶点位置的方法解决倒置的问题,即调整为纹理左上对应顶点左下, 纹理右上对应顶点右下.

    • 图片被倒置的原理解析图
    图片被倒置的原理解析.png

    调整纹理坐标

    let attrArr: [GLfloat] = [
                0.5,  -0.5, -1.0,    1.0, 1.0,
                -0.5, 0.5,  -1.0,    0.0, 0.0, //左上
                -0.5, -0.5, -1.0,    0.0, 1.0, //左下
                
                0.5,  0.5, -1.0,     1.0, 0.0, //右上
                -0.5, 0.5, -1.0,     0.0, 0.0,
                0.5, -0.5, -1.0,     1.0, 1.0, //右下
            ]
    
    • 调整纹理坐标对应的顶点坐标之后的对应关系图
    我们想要的效果原理.png
    在绘制之前对 context 进行平移, 缩放也是可以的, 由于缩放和平移的顺序不同, 所以参数也有所变化.
    • 先平移, 再缩放
            // 5.在CGContext上--> 将图片绘制出来
            let rect = CGRect(x: 0, y: 0, width: spWidth, height: spHeight)
            
            //通过翻转上下文方式, 将图片反转,
            spriteContext?.translateBy(x: 0, y: CGFloat(spHeight))
            spriteContext?.scaleBy(x: 1, y: -1)
            
            // 6.使用默认方式绘制
            spriteContext?.draw(spriteImage, in: rect)
    
    • 先缩放, 再平移
            // 5.在CGContext上--> 将图片绘制出来
            let rect = CGRect(x: 0, y: 0, width: spWidth, height: spHeight)
            
            //通过翻转上下文方式, 将图片反转,
            spriteContext?.scaleBy(x: 1, y: -1)
            spriteContext?.translateBy(x: 0, y: -CGFloat(spHeight))
            
            // 6.使用默认方式绘制
            spriteContext?.draw(spriteImage, in: rect)
    

    注意:
    1.顶点着色器和片元着色器中不能有注释, 特别是中文注释, 有时候会报一些奇怪的错误, 而且很难查找.
    2.着色器代码是写在空文件里的, 相当于字符串, Xcode 没有提示, 文件一般这样命名 xxx.vsh, xxx.fsh, .vsh 表示顶点着色器, .fsh 表示片元着色器,
    3.着色器程序一定不能忘记写分号( ; ).
    4.创建着色器文件时候用的空文件,文件名称和后缀都自己定.

    着色器文件创建.png

    相关文章

      网友评论

          本文标题:OpenGL ES加载图片

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