在 Cocoa 中使用 Swift 3.0 创建 OpenGL 程序

发布于: 2016-10-02 00:17
阅读: 3077
评论: 0
喜欢: 9

本项目为使用Swift 3.0编写的OpenGL Demo。项目在「GitHub」中开源。

导语

本文适合有OpenGL基础Swift语言基础以及曾在 C/C++ 中开发过OpenGLOpenGL SL程序的读者。Demo将在 Cocoa 中使用 Swift 语言创建一个简单的 OpenGL 程序。

Demo效果

开发环境

macOS 10.12 + Xcode 8.0。

StoryBoard

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

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

对象结构

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

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

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

GLView

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

GLScene

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

GLCamera

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

override func glInit()
override func glResize(width: CGFloat, height: CGFloat)
override func glDraw()
override func glDeinit()

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

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

weak var view: GLView!

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

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

CGLLockContext(context.cglContextObj!)
//Draw...
CGLUnlockContext(context.cglContextObj!)

GLObject

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

override func draw()

GLSquare

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

GLCube

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

GLTexConfig

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

    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 :贴图图像类型,可以是RGBYUVOES扩展等,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进行处理。 #扩展 Swift 语言对于指针的支持十分不友好,因此在OpenGL接口和Swift程序之间封装中间层是必要的。 ###GLExtension GLExtension是我封装的一系列类方法的集合。它是整个OpenGL程序的核心,是它驱动着整个OpenGL程序运行。

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

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))来处理回调,也可以使用CVDisplayLinkSetOutputHandle接口,传入一个闭包来处理回调。

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

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数据是没有意义的,创建纹理时数据就已经交给显存了。修改后的贴图处理程序为:

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中的槽位备用。

static func gleBuildShader(sourceFilename: String, type: String = default, shaderType: GLenum) -> GLuint
static func gleBuildProgram(vertexHandle: GLuint, fragmentHandle: GLuint) -> GLuint

创建ShaderglShaderSource接口接收一个UnsafePointer<UnsafePointer<GLchar>?>!类型指针来获得Shader程序数据。在 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后渲染的程序。这些程序较常规就不再讨论了。

Extension

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

extension GLuint {
    var pointer: UnsafeMutablePointer<GLuint> {
        mutating get {
            var pointer: UnsafeMutablePointer<GLuint>!
            withUnsafeMutablePointer(to: &self, { ptr in
                pointer = ptr
            })
            return pointer
        }
    }
}

当OpenGL接口需要一个UnsafeRawPointerUnsafePointer时,直接传入Array可以自动转换。但当接口需要UnsafeMutablePointer时必须实现这类mutating方法来暴露可变指针。暴露指针和指针强转的主要的方法:

func withUnsafeMutablePointer<T, Result>(to arg: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
func unsafeBitCast<T, U>(_ x: T, to: U.Type) -> U

OpenGL程序

当封装完上面的准备工作以后,创建OpenGL程序变得十分简单了。

初始化 —— glInit()

初始化函数中所要做的事:

  • 打开贴图开关和深度测试开关、指定深度测试函数。
        glEnable(GLenum(GL_TEXTURE_2D))
        glEnable(GLenum(GL_DEPTH_TEST))
        glDepthFunc(GLenum(GL_LESS))
  • 调用GLExtension中封装好的方法创建纹理
static func gleGenTexture(idPtr: UnsafeMutablePointer<GLuint>)
static func gleUpdateRGBTexture(id: GLuint, filename: String, type: String? = default)
  • 调用GLExtension中封装好的方法创建Shader程序
static func gleBuildShader(sourceFilename: String, type: String = default, shaderType: GLenum) -> GLuint
static func gleBuildProgram(vertexHandle: GLuint, fragmentHandle: GLuint) -> GLuint
  • 获取Shader程序中参数的槽为以备上传
func glGetAttribLocation(_ program: GLuint, _ name: UnsafePointer<GLchar>!) -> Int32
func glGetUniformLocation(_ program: GLuint, _ name: UnsafePointer<GLchar>!) -> Int32

重置窗口大小 —— glResize(width:height:)

初始化函数中所要做的事:

  • 重置视图大小
glViewport(0, 0, GLsizei(width), GLsizei(height))
  • 重置视锥
func glFrustum(_ left: GLdouble, _ right: GLdouble, _ bottom: GLdouble, _ top: GLdouble, _ zNear: GLdouble, _ zFar: GLdouble)
  • 重置观察矩阵和世界矩阵
glMatrixMode(GLenum(GL_PROJECTION))
glLoadIdentity()
glMatrixMode(GLenum(GL_MODELVIEW))
glLoadIdentity()

绘制方法 —— glDraw()

绘制函数中所要做的事:

  • 清除屏幕
glClearColor(0, 0.2, 0.5, 1.0)
  • 清除颜色缓冲区和深度缓冲区
glClear(GLbitfield(GL_COLOR_BUFFER_BIT) | GLbitfield(GL_DEPTH_BUFFER_BIT))
  • 矩阵变换
func glTranslatef(_ x: GLfloat, _ y: GLfloat, _ z: GLfloat)
func glRotated(_ angle: GLdouble, _ x: GLdouble, _ y: GLdouble, _ z: GLdouble)
  • 绘制
  • 强制执行以上OpenGL程序
glFlush()

清理方法 —— glDeinit()

  清理函数中所要做的事:

  • 删除贴图和Shader程序
func glDeleteTextures(_ n: GLsizei, _ textures: UnsafePointer<GLuint>!)
func glDeleteProgram(_ program: GLuint)

Cocoa交互

与Cocoa的交互中,除了NSOpenGLView中的draw方法外还需实现对OpenGL环境的配置,以及几个简单的鼠标操作事件。这些事件就自然交给ViewController实现了。

  • 创建OpenGL场景并初始化摄像机
    @IBOutlet weak var glView: MainOpenGLView!
    
    var scene: MainScene!
    var cam: MainCamera!

    override func viewDidAppear() {
        setObj()
    }
    
    func setObj() {
        scene = MainScene.init()
        cam = MainCamera.init(view: glView, scene: scene,
                              pos: GLEV3.init(0, 0, 10), center: GLEV3.init(0, 0, 0))
        scene.setObjs()
        cam.glRun()
    }
  • 捕获鼠标点击、拖动以及滚轮动作,相应调整摄像机位置
var lastMousePos: NSPoint?
override func mouseDown(with event: NSEvent) {
    lastMousePos = event.locationInWindow
}
    
override func mouseDragged(with event: NSEvent) {
    if let pos = lastMousePos {
        let newPos = event.locationInWindow
        let dx = newPos.x - pos.x
        let dy = newPos.y - pos.y
        cam.dir.x -= Float(dy)
        cam.dir.z += Float(dx)
        lastMousePos = newPos
    }
}
    
override func scrollWheel(with event: NSEvent) {
    cam.pos.z -= min(max(Float(event.scrollingDeltaY), -1), 1)
}
    
override func mouseUp(with event: NSEvent) {
    lastMousePos = nil
}

总结

本次Demo是一次尝试。Swift不太适合用来写使用大量C接口的程序。指针和选项强制枚举写起来会十分麻烦,但终于也是封装出了一点东西。当然,这样的封装弊端非常明显,物体拆分成三角形一个一个绘制效率会非常低下,当绘制复杂图形时效率会非常低。使用VBO等技术来一次性上传顶点至显存再绘制效率会高很多。总之,本Demo也算是可以给想在macOS里用Swift开发OpenGL程序的读者一点参考吧。


Thanks for reading.

All the best wishes for you! 💕