OpenGL ES加载图片

2020-08-05  本文已影响0人  AndyGF

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
上一篇下一篇

猜你喜欢

热点阅读