本项目为使用Swift 3.0编写的OpenGL Demo。项目在 Github中开源,下载地址见文末。




1、引言

本文适合有OpenGL基础和Swift语言基础的读者,相信敏捷大拇指上的Swift开发者都有这基础。本文将带领曾在 C/C++ 中开发过OpenGL和OpenGL SL程序的读者在 Cocoa 中使用 Swift 语言创建一个简单的 OpenGL 程序。




2、Demo效果

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 1

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 - 敏捷大拇指 - 在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 1


在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 2

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 - 敏捷大拇指 - 在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 2


在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 3

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 - 敏捷大拇指 - 在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 3





3、开发环境

macOS 10.12 + Xcode 8.0。




4、StoryBoard

Storyboard中存在一个NSOpenGLView控件,它会为我们创建好了OpenGL上下文(Context),可以直接使用。我们的所有OpenGL代码都依赖着这个上下文执行。在界面方面直接将其加入到界面中,附上全屏的AutoLayout,界面就设计完毕了。

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 4

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 - 敏捷大拇指 - 在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 4


使用 glGetString(GLenum(GL_VERSION)) 可以看到自动创建的上下文版本为OpenGL 2.1,这个版本是支持Shader的,因此后面我们会用到OpenGL SL语言来处理图元和顶点。




5、对象结构

在我设计的封装中,一个完整的OpenGL程序需要有:

  • 窗口(View):直接显示在屏幕中,其继承于NSOpenGLView,与Cocoa交互。
  • 摄像机(Camera):摄像机是观察的媒介。不同位置、方向、不同观察模式的摄像机将会呈现不同的视觉效果。因此我们的OpenGL程序包括Shader程序主要存在于摄像机中,不同的摄像机可以以不同的渲染程序去观察场景中的物体。
  • 场景(Scene):场景中仅仅统一了物体,维护当前场景中的物体供摄像机调用。
  • 物体(Object):物体是主要的观察对象。包含了大小、方向、位置、贴图等等信息。


在源码中的GLObject目录中可以看到我封装的一些基类,读者在开发自己的OpenGL程序时可以以这些类作为基类来继承出自己的子类。



5.1、GLView

继承于NSOpenGLView,重写了draw方法(Swift 3.0以前为drawRect),当View大小改变时Cocoa会回调draw方法,此时我们可以通知OpenGL程序窗口的大小改变了。



5.2、GLScene

提供了物体的容器以及方法,创建子类后可以在子类创建物体。



5.3、GLCamera

OpenGL渲染程序写在这个类的子类中。需要重写的方法有:

[Swift] 纯文本查看 复制代码
override func glInit()
override func glResize(width: CGFloat, height: CGFloat)
override func glDraw()
override func glDeinit()


其中glDraw()方法将会在被CVDisplayLink中每秒60次调用,其具体实现在GLExtension中,将在下文讨论。最主要的OpenGL程序将在上面四个方法中实现。

在执行绘制的OpenGL代码前,我们必须获得需要显示的NSOpenGLView所创建的上下文,并设置为当前上下文:

[Swift] 纯文本查看 复制代码
weak var view: GLView!

let context = view.openGLContext!
context.makeCurrentContext()


OpenGL代码可能会同时执行在多个线程里。例如常规的绘制函数由CVDisplayLink驱动,此时用户改变了窗口的大小,那么GLView会通知OpenGL执行窗口变换的代码,此时将会发生两个线程同时操作同一个上下文的情况,会导致程序崩溃。在这里为了安全起见,执行前需加锁,之行结束后解锁:

[Swift] 纯文本查看 复制代码
CGLLockContext(context.cglContextObj!)
//Draw...
CGLUnlockContext(context.cglContextObj!)




5.4、GLObject

物体基类。包含物体的大小、方向、位置数据。另外GLObject遵守GLDrawable协议,需实现draw方法,供绘制时调用。需要重写方法:

[Swift] 纯文本查看 复制代码
override func draw()




5.5、GLSquare

封装好的方形平面。可以直接传入坐标和贴图配置绘制出来。其具体实现在GLExtension中,将在下文讨论。



5.6、GLCube

封装好的长方体。可以直接传入坐标和贴图配置绘制出来。其具体实现在GLExtension中,将在下文讨论。



5.7、GLTexConfig

贴图配置的封装,其构造方法原型为:

[Swift] 纯文本查看 复制代码
    init(type: GLTexType, texID: GLuint? = nil,
         texWidthRepeat: Float? = 1, texHeightRepeat: Float? = 1,
         width: GLfloat? = nil, height: GLfloat? = nil, yuvIDs: Array<GLuint>? = nil, yuvData: NSData? = nil,
         alpha: Float = 1)


  • type :贴图图像类型,可以是RGB、YUV、OES扩展等,Demo中仅实现了RGB贴图。其余贴图的绘制可以直接在源码中扩展,直接实现相应方法即可。
  • texID:贴图在OpenGL中的槽位。
  • texWidthRepeat:贴图在横向的重复次数。由于封装的贴图函数的GL_TEXTURE_WRAP_S参数为GL_REPEAT,而贴图坐标映射是在封装中自动计算的,因此这里可以指定重复次数,默认为1。
  • texHeightRepeat:贴图在纵向的重复次数,同上。
  • width, height, yuvIDs, yuvData:贴图长宽、YUV贴图的ID数组(yuv贴图如果采用多重纹理,则ID数组至少有3个元素)、YUV数据。这些在绘制YUV数据时才需要指定。YUV贴图在Demo中没有实现具体方法,但预留了实现的空间,读者可以自己扩展。YUV转RGB多重纹理的Shader函数在Demo中已经给出。
  • alpha:贴图透明度。在每个GLObject的绘制函数中均会将贴图配置传递至GLExtension进行处理。





6、扩展

Swift 语言对于指针的支持十分不友好,因此在OpenGL接口和Swift程序之间封装中间层是必要的。



6.1、GLExtension

GLExtension是我封装的一系列类方法的集合。它是整个OpenGL程序的核心,是它驱动着整个OpenGL程序运行。

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 5

在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 - 敏捷大拇指 - 在 Cocoa 中使用 Swift3.0 创建 OpenGL 程序 5


OpenGL绘制程序的驱动依赖CVDisplayLink,它会在每次显示器刷新时调用绘制函数,使OpenGL程序与显示器刷新率保持同样帧率。CVDisplayLink存在于CoreVideo中,其具体实现代码为:

[Swift] 纯文本查看 复制代码
    import CoreVideo

    static var displayLink: CVDisplayLink?
    static func gleRun(sender: GLCamera) {
        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
        CVDisplayLinkSetOutputHandler(displayLink!) { (_, _, _, _, _) -> CVReturn in
            sender._glDraw()
            return kCVReturnSuccess
        }
        CVDisplayLinkStart(displayLink!)
    }

    static func gleStop() {
        CVDisplayLinkStop(displayLink!)
    }


CVDisplayLink接口为非常典型的C接口,CVDisplayLinkSetOutputCallback接收一个C函数指针( @convention(c) )来处理回调,找了很多方法都没有在Swift中成功调用。幸运的是,CVDisplayLinkSetOutputHandle接口允许传入一个闭包来处理回调。

在处理贴图上使用CGImage在CGContext上draw的方法来取得图像数据指针交至glTexImage2D接口处理贴图:

[Swift] 纯文本查看 复制代码
    static func gleUpdateRGBTexture(id: GLuint, image: CGImage) {
        let width = image.width
        let height = image.height
        let dataSize = width * height * 4
        let data = UnsafeMutablePointer<UInt8>.allocate(capacity: dataSize)
        let colorSpace = image.colorSpace!
        let context = CGContext.init(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
        context.draw(image, in: CGRect.init(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
        glBindTexture(GLenum(GL_TEXTURE_2D), id)
        glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
        glBindTexture(GLenum(GL_TEXTURE_2D), 0)
        free(data)
    }


在CGImage释放池的处理上使用autoreleasepool{ }来及时释放CGImage占用的内存,因为在OpenGL程序里在内存中缓存CGImage数据是没有意义的,创建纹理时数据就已经交给显存了。修改后的贴图处理程序为:

[Swift] 纯文本查看 复制代码
    static func gleUpdateRGBTexture(id: GLuint, image: CGImage) {
        let width = image.width
        let height = image.height
        let dataSize = width * height * 4
        let data = UnsafeMutablePointer<UInt8>.allocate(capacity: dataSize)
        autoreleasepool {
            let colorSpace = image.colorSpace!
            let context = CGContext.init(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
            context.draw(image, in: CGRect.init(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
            glBindTexture(GLenum(GL_TEXTURE_2D), id)
            glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
            glBindTexture(GLenum(GL_TEXTURE_2D), 0)
        }
        free(data)
    }


着色器(Shader)程序的创建也在这里做了封装,以下两个方法可以创建并获得着色器程序在OpenGL中的槽位备用。

[Swift] 纯文本查看 复制代码
static func gleBuildShader(sourceFilename: String, type: String = default, shaderType: GLenum) -> GLuint
static func gleBuildProgram(vertexHandle: GLuint, fragmentHandle: GLuint) -> GLuint


创建Shader时glShaderSource接口接收一个UnsafePointer<UnsafePointer<GLchar>?>!类型指针来获得Shader程序数据。在 Swift 中获取这样的一个指针又是一件麻烦的事情。解决方案为多次暴露指针和强转:

[Swift] 纯文本查看 复制代码
        //读取文本
        let sourcePath = Bundle.main.path(forResource: sourceFilename, ofType: type)!
        let sourceData = NSData.init(contentsOfFile: sourcePath)!
        //获取长度
        var dataSize: GLint = GLint(sourceData.length)
        //创建Byte数组和各项指针
        let dataBytes = UnsafeMutableRawPointer.allocate(bytes: Int(dataSize), alignedTo: 0)
        var sourcePtr = unsafeBitCast(dataBytes, to: UnsafePointer<GLchar>.self)
        var sourcePtrPtr: UnsafePointer<UnsafePointer<GLchar>?>!
        withUnsafePointer(to: &sourcePtr, { ptr in
            sourcePtrPtr = unsafeBitCast(ptr, to: UnsafePointer<UnsafePointer<GLchar>?>.self)
        })
        //填充
        sourceData.getBytes(dataBytes, range: NSRange.init(location: 0, length: Int(dataSize)))
        glShaderSource(shaderHandle, 1, sourcePtrPtr, dataSize.pointer)
        free(dataBytes)
        glCompileShader(shaderHandle)


关于Shader程序本身,顶点函数中只包含了顶点矩阵变换,图元函数中包含了RGB混合alpha直接渲染和YUV转RGB后渲染的程序。这些程序较常规就不再讨论了。



6.2、Extension

对于关键的数据类型,我们可以写一些计算属性来暴露数据指针,例如GLuint指针的暴露方法:

[Swift] 纯文本查看 复制代码
extension GLuint {
    var pointer: UnsafeMutablePointer<GLuint> {
        mutating get {
            var pointer: UnsafeMutablePointer<GLuint>!
            withUnsafeMutablePointer(to: &self, { ptr in
                pointer = ptr
            })
            return pointer