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

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看339 回复回复2 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 339|回复: 2
收起左侧

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

[复制链接]
swifter 发表于 2016-10-13 04:02:36 | 显示全部楼层 |阅读模式
快来登录
获取最新的苹果动态资讯
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码

本项目为使用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
        }
    }
}


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

[Swift] 纯文本查看 复制代码
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





7、OpenGL程序

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



7.1、初始化 —— glInit()

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

  • 打开贴图开关和深度测试开关、指定深度测试函数。


[Swift] 纯文本查看 复制代码
      glEnable(GLenum(GL_TEXTURE_2D))
      glEnable(GLenum(GL_DEPTH_TEST))
      glDepthFunc(GLenum(GL_LESS))


  • 调用GLExtension中封装好的方法创建纹理


[Swift] 纯文本查看 复制代码
static func gleGenTexture(idPtr: UnsafeMutablePointer<GLuint>)
static func gleUpdateRGBTexture(id: GLuint, filename: String, type: String? = default)


  • 调用GLExtension中封装好的方法创建Shader程序


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


  • 获取Shader程序中参数的槽为以备上传


[Swift] 纯文本查看 复制代码
func glGetAttribLocation(_ program: GLuint, _ name: UnsafePointer<GLchar>!) -> Int32
func glGetUniformLocation(_ program: GLuint, _ name: UnsafePointer<GLchar>!) -> Int32




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

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

  • 重置视图大小


[Swift] 纯文本查看 复制代码
      glViewport(0, 0, GLsizei(width), GLsizei(height))


  • 重置视锥


[Swift] 纯文本查看 复制代码
      func glFrustum(_ left: GLdouble, _ right: GLdouble, _ bottom: GLdouble, _ top: GLdouble, _ zNear: GLdouble, _ zFar: GLdouble)


  • 重置观察矩阵和世界矩阵


[Swift] 纯文本查看 复制代码
      glMatrixMode(GLenum(GL_PROJECTION))
      glLoadIdentity()
      glMatrixMode(GLenum(GL_MODELVIEW))
      glLoadIdentity()




7.3、绘制方法 —— glDraw()

绘制函数中所要做的事:

  • 清除屏幕


[Swift] 纯文本查看 复制代码
      glClearColor(0, 0.2, 0.5, 1.0)


  • 清除颜色缓冲区和深度缓冲区


[Swift] 纯文本查看 复制代码
      glClear(GLbitfield(GL_COLOR_BUFFER_BIT) | GLbitfield(GL_DEPTH_BUFFER_BIT))


  • 矩阵变换


[Swift] 纯文本查看 复制代码
      func glTranslatef(_ x: GLfloat, _ y: GLfloat, _ z: GLfloat)
      func glRotated(_ angle: GLdouble, _ x: GLdouble, _ y: GLdouble, _ z: GLdouble)


  • 绘制
  • 强制执行以上OpenGL程序


[Swift] 纯文本查看 复制代码
      glFlush()




7.4、清理方法 —— glDeinit()

清理函数中所要做的事:

  • 删除贴图和Shader程序


[Swift] 纯文本查看 复制代码
func glDeleteTextures(_ n: GLsizei, _ textures: UnsafePointer<GLuint>!)
func glDeleteProgram(_ program: GLuint)





8、Cocoa交互

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

  • 创建OpenGL场景并初始化摄像机


[Swift] 纯文本查看 复制代码
  @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()
  }


  • 捕获鼠标点击、拖动以及滚轮动作,相应调整摄像机位置


[Swift] 纯文本查看 复制代码
  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
  }





9、总结

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





下载:

github地址:

游客,如果您要查看本帖隐藏内容请回复



都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

本帖被以下淘专辑推荐:

攻城狮 发表于 2016-10-14 11:42:15 | 显示全部楼层
iOS里什么情景下使用OpenGL编程?
我叫床 发表于 2016-10-25 23:55:03 | 显示全部楼层
回复看看
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表