OpenGL ES之Swift使用GLSL语言渲染图片的显示

整体思路

一、GLSL和iOS
  • 本片主要使用编译链接自定义的shader(顶点着色器/片元着色器)。用简单的GLSL语言来实现顶点、片元着色器,并对图形进行简单的变换。
  • OpenGL ES只是用来做渲染的,所iOS要提供一个载体,就是CAEAGLLayer,创建的方法是,通过重写UIView的类属性(OC中是类方法)返回CAEAGLLayer.self。它是一个对core animation的封装,它能满足所有的OpenGLES的方法访问。
  • CAEAGLLayer:在制定该图层关联的视图作为渲染器的目标图形上下文之前,可以使用drawableProperties属性更改呈现属性。此属性允许您配置呈现表面的颜色格式以及表面是否保留其内容。 因为OpenGL ES渲染的效果是要提交到用户使用的核心动画上,所以使用在该layer上的任何效果和动画都会影响渲染的3D效果,为了时性能最佳你应该做一下操作:设置图层为不透明,设置图层边界以匹配显示的尺寸,确保图层没有做变换。
  • 尽量避免在CAEAGLLayer添加其layer。如果必须要添加其他非OpenGL内容,那么如果将透明的2D内容置于GL内容之上,并确保OpenGL内容是不透明的且没有转换过,那么性能还是可以接受的。当在竖屏上绘制横向内容时,应该自己旋转内容,而不是使用CAEAGLLayer转换来旋转它。
二、GLSL实现图片渲染的思路
  • 创建图层:重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer,并设置描述属性;
  • 创建上下文:上下文主要是用于保存OpenGL ES中的状态,是一个状态机,不论是GLKIt还是GLSL,都是需要context的;
  • 清空缓存区:buffer分为 frameBuffer 和 renderBuffer 两个大类,都需要清空;
  • 设置RenderBuffer:渲染缓存区
  • 设置FrameBuffer:帧缓存区
  • 开始绘制:读取顶点/片元着色器的程序,并加载shader,编译顶点着色程序/片元着色器程序,然后链接程序,设置顶点、纹理坐标,并处理顶点坐标和纹理;
  • 析构函数中释放buffer。
三、GLSL着色语言

准备工作

一、ViewController和View
  • 新建一个view继承UIView,命名为YDWView;
  • 在ViewController中声明一个var myView : YDWView!属性。在viewDidLoad加载myView:
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.myView = self.view as? YDWView
    }
  • 在YDWView中定义部分需要用到的全局变量(在iOS和tvOS上绘制OpenGL ES内容的图层,继承CALayer),并重载layoutSubviews和重写layerClass方法(重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer):
    var myEagLayer : CAEAGLLayer!
    var myContext  : EAGLContext!
    
    var myColorRenderBuffer : GLuint!
    var myColorFrameBuffer : GLuint!
    
    var myPrograme : GLuint!
    
    override func layoutSubviews() {
        
    }
    
    override class var layerClass: AnyClass {
        return CAEAGLLayer.self
    }
二、自定义着色器的vsh和fsh文件创建
  • 创建shader.vsh文件(顶点着色器):
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main() {
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

  • 创建shaderv.fsh文件(片元着色器):将gl_FragColor = texture2D(colorMap, varyTextCoord);改为gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));翻转纹理;
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main() {
    
    gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));

}

渲染流程

一、创建图层
  • 创建特殊图层并设置scale
    // 创建图层
    private func setupLayer() {
        self.myEagLayer = self.layer as? CAEAGLLayer
        self.contentScaleFactor = UIScreen.main.scale
        
        self.myEagLayer.drawableProperties = [kEAGLDrawablePropertyRetainedBacking : false,
                                              kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8]
        
    }
二、创建上下文
  • 创建图形上下文,指定OpenGL ES 渲染API版本;
  • 判断是否创建成功和设置图形上下文是否成功;
  • 将局部context,变成全局的context;
	// 创建上下文
    private func setupContext() {
        // 创建图形上下文,并判断是否创建成功
        if let context = EAGLContext.init(api: .openGLES2) {
            EAGLContext.setCurrent(context)
            // 将局部context,变成全局的context
            self.myContext = context
        } else {
            print("Failed")
        }
        
    }
三、清空缓存区
    // 清空缓存区
    private func clearRenderAndFrameBuffer() {

        glDeleteBuffers(1, &myColorRenderBuffer)
        self.myColorRenderBuffer = 0

        glDeleteBuffers(1, &myColorFrameBuffer)
        self.myColorFrameBuffer = 0
    }
四、 设置RenderBuffer(渲染缓存区)
	// 设置RenderBuffer
    private func setupRenderBuffer() {
        // 申请一个缓冲区标志
        glGenBuffers(1, &self.myColorRenderBuffer)

        // 将标识符绑定到GL_RENDERBUFFER
        glBindRenderbuffer(GLenum(GL_RENDERBUFFER), self.myColorRenderBuffer)
        // 将可绘制对象drawable object's  CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
        self.myContext.renderbufferStorage(Int(GL_RENDERBUFFER), from: self.myEagLayer)

    }
五、设置FrameBuffer(帧缓存区)
	// 设置FrameBuffer
    private func setupFrameBuffer() {
        // 申请一个缓冲区标志
        glGenBuffers(1, &self.myColorFrameBuffer)

        // 将标识符绑定到GL_FRAMEBUFFER
        glBindFramebuffer(GLenum(GL_FRAMEBUFFER), self.myColorFrameBuffer)
        // 将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
        glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), self.myColorRenderBuffer)
    }
  • 生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,绘制才能起作用。
六、开始绘制
  • 设置背景颜色和视口
		// 设置背景颜色
        glClearColor(0.4, 0.4, 0.4, 1.0)
        // 清除深度缓冲区
        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))

        // 设置视口
        let scale = UIScreen.main.scale
        glViewport(GLint(self.frame.origin.x * scale), GLint(self.frame.origin.y * scale), GLsizei(self.frame.size.width * scale), GLsizei(self.frame.size.height * scale))
  • 读取顶点着色程序、片元着色程序
		// 读取顶点着色程序、片元着色程序
        let vertFile : String = Bundle.main.path(forResource: "shader", ofType: "vsh") ?? ""
        let fragFile : String = Bundle.main.path(forResource: "shaderf", ofType: "fsh") ?? ""
        let (sucess, program) = self.loadShaders(vert: vertFile, frag: fragFile)
        if !sucess! {
            return
        }
        self.myProgram = program

  • 加载着色器shader并判断是否链接成功
// 加载着色器程序方法
private func loadShaders(vert:String, frag:String) -> (Bool?, GLuint?) {

        let program : GLuint = glCreateProgram()

        guard let verShader : GLuint = self.compileShader(type: GLenum(GL_VERTEX_SHADER), filePath: vert) else {
            return (false, program)
        }
        // 把编译后的顶点着色器代码附着到最终的程序上
        glAttachShader(program, verShader)
        // 释放不需要的shader
        glDeleteShader(verShader)

        guard let fragShader : GLuint = self.compileShader(type: GLenum(GL_FRAGMENT_SHADER), filePath: frag) else {
            return (false, program)
        }
        // 把编译后的片元着色器代码附着到最终的程序上
        glAttachShader(program, fragShader)
        // 释放不需要的shader
        glDeleteShader(fragShader)
        
        // 链接着色器代程序
        glLinkProgram(program)

  		// 获取链接状态
        var status : GLint = 0
        glGetProgramiv(program, GLenum(GL_LINK_STATUS), &status)
        if status == GLenum(GL_FALSE){
            print("Link Error")
            // 打印错误信息
            let message = UnsafeMutablePointer<GLchar>.allocate(capacity: 512)
            glGetProgramInfoLog(program, GLsizei(MemoryLayout<GLchar>.size * 512), nil, message)
            let string = String.init(utf8String: message)
            print("program link error \(string!)")
            return (false, program)
        } else {
            print("link sucess!")
            // 链接成功,使用着色器程序
            glUseProgram(program)
            return (true, program)
        }
    }
  • 编译着色器程序
 	// 编译着色器程序
    private func compileShader(type:GLenum, filePath:String) -> GLuint? {
         // 创建一个空着色器
         let verShader:GLuint = glCreateShader(type)
         // 获取源文件中的代码字符串
         guard let shaderString = try? String.init(contentsOfFile: filePath, encoding: String.Encoding.utf8) else {
             return nil
         }
         // 转成C字符串赋值给已创建的shader
         shaderString.withCString { (pointer) in
             var pon:UnsafePointer<GLchar>? = pointer
             glShaderSource(verShader, 1, &pon, nil)
         }
         // 编译
         glCompileShader(verShader)

         return verShader
    }
  • 设置顶点、纹理坐标,并处理顶点坐标和纹理
		// 设置顶点、纹理坐标
        let vertexs : [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,
        ]

        // 处理顶点数据(copy到缓冲区)
        var verbuffer = GLuint()
        glGenBuffers(1, &verbuffer)
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), verbuffer)
        glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * 30, vertexs, GLenum(GL_DYNAMIC_DRAW))

        /* 将顶点数据通过Programe传递到顶点着色程序的position属性上
         * glGetAttribLocation,用来获取vertex attribute的入口
         * 告诉OpenGL ES,通过glEnableVertexAttribArray,打开开关
         * 结果数据是通过glVertexAttribPointer传递过去的
         */

        // 顶点坐标:第个二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
        let position = glGetAttribLocation(self.myProgram, "position")
        // 设置合适的格式从buffer里面读取数据
        glEnableVertexAttribArray(GLuint(position))
        /* 设置读取方式
         * 参数1:index,顶点数据的索引
         * 参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
         * 参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
         * 参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
         * 参数5:stride,连续顶点属性之间的偏移量,默认为0;
         * 参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件,默认为0
         */
        glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: 0))
        // 纹理坐标数据通过Programe传递到顶点着色程序的textCoordinate属性上
        let texture = glGetAttribLocation(myProgram, "textCoordinate")
        glEnableVertexAttribArray(GLuint(texture))
        glVertexAttribPointer(GLuint(texture), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 3))

        // 加载纹理图片
        setUpTextureImage(imageName: "yiyi")
        // 设置纹理采样器
        glUniform1i(glGetUniformLocation(self.myProgram, "colorMap"), 0)
        // 绘制
        glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
        // 提交
        self.myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
  • 从图片中加载纹理
	// 从图片中加载纹理
    private func setUpTextureImage(imageName:String) {
        guard let image = UIImage.init(named: imageName)?.cgImage else {
            return
        }

        let width = image.width
        let height = image.height

        // 开辟内存,绘制到内存上去
        let spriteData: UnsafeMutablePointer = UnsafeMutablePointer<GLubyte>.allocate(capacity: MemoryLayout<GLubyte>.size * width * height * 4)

        UIGraphicsBeginImageContext(CGSize.init(width: width, height: height))

        let spriteContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: image.width * 4, space: image.colorSpace!, bitmapInfo: image.bitmapInfo.rawValue)

        spriteContext?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))

        UIGraphicsEndImageContext()

        // 绑定纹理
        glBindTexture(GLenum(GL_TEXTURE_2D), 0)
        // 设置纹理参数:缩小/放大过滤器
        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)
        /* 载入纹理
         * 参数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(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(image.width), GLsizei(image.height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE),spriteData)
        // 释放内存
        free(spriteData)
    }
    
  • 稀构方法中清空缓冲区
deinit {
    glDeleteBuffers(1, &myColorRenderBuffer)
    glDeleteBuffers(1, &myColorFrameBuffer)
}

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/107704688