如何将 OpenGL GLKView 转换为基于 MTLKit Metal 的视图?

Dav*_*d H 4 opengl ios metal metalkit

几年前,Apple 开始警告任何在其应用程序中使用GLKit 的人OpenGL即将消失:

警告:OpenGLES 已弃用。考虑迁移到 Metal,而不是
警告:GLKit 已弃用。考虑迁移到 MetalKit

我的应用程序使用复杂的 OpenGL 类,并且我不知道OpenGLMetal。Apple 有一些关于此的 WWDC 会议,但它们是针对OpenGL专家的。由于 Apple 有一天会删除 OpenGL,所以我想在我只有几个月的时间之前立即开始这项工作。我应该怎么办?

Dav*_*d H 8

太棒了;

\n

当我开始在 iOS12 中看到构建错误消息时:

\n
\n

警告:OpenGLES 已弃用。考虑迁移到 Metal
\n警告:GLKit 已弃用。考虑迁移到 MetalKit

\n
\n

我知道我必须做点什么。谁知道 Apple 何时会删除OpenGLGLKit?相信我,您不想等到几个月的时间才转换为Metal,因为这个过程绝不是直截了当的。

\n

以下是我将Objective-C/OpenGL视图转换为Metal的过程。这是一个漫长而艰辛的过程,有好几次\n我把头伏在桌子上沮丧地哭泣。

\n

我建议其他人也采取我采取的基本步骤:

\n
    \n
  1. 从视图中删除所有业务逻辑以及与OpenGL不直接相关的任何内容,并根据需要重组主应用程序。
  2. \n
  3. 创建一个将用于转换的测试工具应用程序,并将其置于版本控制之下。
  4. \n
  5. OpenGL视图添加到测试工具中。
  6. \n
  7. 一旦ViewController可以驱动视图并且您可以看到它,您就可以开始转换了。
  8. \n
\n

就我而言,我需要克服三个障碍:将视图转换为Swift ,在Metal中重新创建功能,然后将所有GLK向量和矩阵值以及操作替换为simd

\n

我对继续进行的建议:

\n
    \n
  1. 将任何ObjectiveC转换为Swift(我使用Swiftify,免费进行有限翻译,但我有订阅)
  2. \n
  3. MTKView添加到测试工具中,并将代码开关放入ViewController中,以便交替查看任一视图(比较这两个视图对我有很大帮助)。
  4. \n
  5. 由于我既不了解OpenGL也不了解Metal,所以我花了很多时间下载开源Metal项目和教程。
  6. \n
  7. 创建 Metal 样板(基于示例/教程)以及着色器
  8. \n
  9. 在你的桌子上放一个垫子,这样当你沮丧地撞头试图在金属视图中显示任何东西时,你就不会严重伤害自己。
  10. \n
  11. 翻过山后,将GLK值/操作转换为simd,利用稍后显示的转换函数。
  12. \n
\n

我无法强调这一点\xe2\x80\x94 每次更改一些操作并测试它们时都会提交!您肯定会破坏一些东西,这样您就可以引用早期的工作代码。

\n

测试工具将被证明是有用的,因为您可能会发现时间变化会导致不良行为。就我而言,我创建了两个线束,第二个线束中有更多应用程序代码,以便我可以更好地调试实际使用情况。

\n

项目

\n

我分叉了一个开源项目Panorama。master分支包含Metal/simd代码,Swift-OpenGL分支包含原始ObjectiveC代码以及Swift转换。这可以让读者并排比较两者。但是,您不需要参考它来了解如何将ObjectiveC中的OpenGL代码转换为Swift ,或将GLKit向量和矩阵转换为simd,如下所示。

\n

ObjectiveC 到 Swift

\n

OpenGL代码大量使用指针,而这些在Swift中有点麻烦。例如:

\n
GLfloat *m_TexCoordsData;  // treated as an array of pairs of floats\nglTexCoordPointer(2, GL_FLOAT, 0, m_TexCoordsData);\n
Run Code Online (Sandbox Code Playgroud)\n

成为

\n
struct Pointer2 {\n    private var array: [SIMD2<Float>]\n\n    init(size: Int) {\n        let n: SIMD2<Float> = [Float.nan, Float.nan]\n        array = Array<SIMD2<Float>>(repeating: n, count: size)\n    }\n\n    subscript(index: Int) -> SIMD2<Float>{\n        get { return array[index] }\n        set(newValue) { array[index] = newValue }\n    }\n\n    mutating func usingRawPointer(block: WithRawPtr) {\n        array.withUnsafeBytes { (bufPtr) -> Void in\n            block(bufPtr.baseAddress!)\n        }\n    }\n}\n    private var tPtr: Pointer2   // m_TexCoordsData\n    tPtr.usingRawPointer(block: { (ptr) in\n        glTexCoordPointer(2, UInt32(GL_FLOAT), 0, ptr)\n    })\n
Run Code Online (Sandbox Code Playgroud)\n

这一切都在最终的Metal代码中消失了。\n进行转换也发现了潜在的错误!

\n

此外,我必须将许多值转换(转换)为更严格的 Swift 类型:

\n
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);\n
Run Code Online (Sandbox Code Playgroud)\n

成为

\n
glTexParameteri(UInt32(GL_TEXTURE_2D), UInt32(GL_TEXTURE_WRAP_S), GLint(GL_REPEAT))\n
Run Code Online (Sandbox Code Playgroud)\n

这个过程非常乏味。然而,恕我直言, Swift代码更精简,更容易阅读。

\n

几个函数很容易翻译:

\n
GLKQuaternion GLKQuaternionFromTwoVectors(GLKVector3 u, GLKVector3 v) {\n    GLKVector3 w = GLKVector3CrossProduct(u, v);\n    GLKQuaternion q = GLKQuaternionMake(w.x, w.y, w.z, GLKVector3DotProduct(u, v));\n    q.w += GLKQuaternionLength(q);\n    return GLKQuaternionNormalize(q);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

成为

\n
func GLKQuaternionFromTwoVectors(_ u: GLKVector3, _ v: GLKVector3) -> GLKQuaternion {\n    let w = GLKVector3CrossProduct(u, v)\n    var q = GLKQuaternionMake(w.x, w.y, w.z, GLKVector3DotProduct(u, v))\n    q.w += GLKQuaternionLength(q)\n    return GLKQuaternionNormalize(q)\n}\n
Run Code Online (Sandbox Code Playgroud)\n

稍后你会发现翻译成simd并不那么容易。

\n

OpenGL 到金属

\n

不幸的是,这里没有魔杖。苹果公司在 WWDC 上有过一些关于这个问题的会议,但他们并没有真正启发我。Metal使用两种类型的内核:计算和着色器,其中计算更容易。然而,就我而言,我必须使用着色器,这对我来说更难掌握。

\n

金属资源

\n

如果您对Metal一无所知,可以从 Ray Wenderlich 网站上的Metal 教程开始。第二篇文章更有帮助:Ray Wenderlich 网站上的Moving From OpenGL to Metal 。两者都大量引用了更多金属材料。

\n

我发现另外两篇很有帮助的优秀文章:Donald Pinckney 的博客(旧版)。另一位有用的作者:Alex Barbulescu

\n

沃伦·摩尔(Warren Moore)确实写了这本关于金属的书。他的书和文章非常宝贵!

\n

要记住的事情

\n

OpenGL使用-1 到 1(z值)的剪辑空间。您需要在着色器中考虑到这一点。Warren Moore 个人建议我使用以下代码确保我的着色器不会返回负z值:

\n
v.z = v.z * 0.5 + v.w * 0.5;\n
Run Code Online (Sandbox Code Playgroud)\n

这消除了完全重做可能使用负z的 OpenGL 代码的需要值的 OpenGL 代码。

\n

MTLView的backgroundColor不是通过使用该属性来设置的,而是通过设置clearColor设置的。

\n

从应用程序空间到着色器空间的通信是使用结构完成的,必须分别在每个结构中单独定义。例如,在我的应用程序中这个结构:

\n
private struct Uniforms {\n  let projectionMatrix: simd_float4x4\n  let attitudeMatrix: simd_float4x4\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在着色器中定义为:

\n
struct Uniforms {\n  float4x4 projectionMatrix;\n  float4x4 attitudeMatrix;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

这些结构是应用程序定义的。

\n

纹理

\n

如果您使用图像来创建纹理,则Metal中的情况有点不同。这

\n
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], GLKTextureLoaderOriginBottomLeft, nil];\nGLKTextureInfo *info=[GLKTextureLoader textureWithContentsOfFile:path options:options error:&error];\nglBindTexture(GL_TEXTURE_2D, info.name);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, IMAGE_SCALING);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, IMAGE_SCALING);\n
Run Code Online (Sandbox Code Playgroud)\n

成为

\n
let loader: MTKTextureLoader = MTKTextureLoader(device: mtlDevice)\ndo {\n    let texture = try loader.newTexture(cgImage: cgImage, options: [\n        MTKTextureLoader.Option.origin: MTKTextureLoader.Origin.bottomLeft,\n        MTKTextureLoader.Option.SRGB: false // yes and image washed out\n    ])\n    return texture\n} catch {\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,您需要将原点设置为bottomLeft。

\n

最终意见

\n

除非您打算真正深入学习Metal,否则您将进行大量实验并提出问题。当您花费数小时尝试让代码执行您想要的操作时,拥有一个测试工具应用程序将被证明是无价的。

\n

将 GLK 转换为 simd

\n

任何 Apple 代码肯定都充满了 GLKVector、GLKMatrices 和相关函数。不幸的是,没有工具可以转换它们\xe2\x80\x94,你必须逐行手动完成,有时没有simd等效项。有时我使用 Xcode 的搜索和替换,但不经常使用。

\n

GLK->SIMD

\n

首先,要获取 simd 宏,请将其添加到源文件中:import simd

\n
    \n
  • GLfloat -> 浮点
  • \n
  • GLint -> 整数
  • \n
  • GLKMatrix4 -> simd_float4(SIMD4 的类型别名)
  • \n
  • GLKMatrix4Identity->matrix_identity_float4x4(不容易找到)
  • \n
  • GLKMatrix4Invert -> simd_inverse(simd_float4x4)
  • \n
  • GLKMatrix4Make -> simd_float4x4(simd_float4, simd_float4, simd_float4, simd_float4)
  • \n
  • GLKMatrix4MakeFrustum -> 没有替代品,下面提供函数
  • \n
  • GLKMatrix4MakeLookAt -> 无替换,下面提供函数
  • \n
  • GLKMatrix4MakeWithQuaternion -> simd_matrix4x4(simd_quatf)
  • \n
  • GLKMatrix4Multiply -> simd_float4x4 * simd_float4x4
  • \n
  • GLKMatrix4MultiplyVector3 -> 没有替代品,函数如下
  • \n
  • GLKMatrix4MultiplyVector4 ->simd_float4x4 * simd_float4
  • \n
  • GLK四元数 -> simd_quatf
  • \n
  • GLKQuaternionLength -> simd_quatf.length
  • \n
  • GLKQuaternionMake -> simd_quaternion(_ x: 浮点, _y: 浮点, _ z: 浮点, _ w: 浮点)
  • \n
  • GLKQuaternionNormalize -> simd_quatf.normalized
  • \n
  • GLKTextureInfo -> MTLTexture
  • \n
  • GLKVector3 -> simd_float3
  • \n
  • GLKVector3CrossProduct -> simd_cross(simd_float3, simd_float3)
  • \n
  • GLKVector3DotProduct -> simd_dot(simd_float3, simd_float3)
  • \n
  • GLKVector3Make -> simd_make_float3(_ x: 浮点, _y: 浮点, _ z: 浮点)
  • \n
  • GLKVector3Normalize -> simd_normalize(simd_float3)
  • \n
  • GLKVector4 -> simd_float4
  • \n
  • GLKVector4Make -> simd_make_float4(_ x: 浮点, _y: 浮点, _ z: 浮点, _ w: 浮点)
  • \n
\n

我应该指出,Dash在挖掘simd方面提供了巨大的帮助

\n

上面引用的两个函数:

\n
func simd_make_look_at_float4x4(\n                                eyeX: Float,\n                                eyeY: Float,\n                                eyeZ: Float,\n                                centerX: Float,\n                                centerY: Float,\n                                centerZ: Float,\n                                upX: Float,\n                                upY: Float,\n                                upZ: Float\n) -> simd_float4x4 {\n    // /sf/ask/633736421/\n    let ev = simd_float3(eyeX, eyeY, eyeZ)\n    let cv = simd_float3(centerX, centerY, centerZ)\n    let uv = simd_float3(upX, upY, upZ)\n\n    let subbed = ev - cv\n    let n = simd_normalize(subbed)\n\n    let cross_p = simd_cross(uv, n)\n    let u = simd_normalize(cross_p)\n\n    let v = simd_cross(n, u)\n\n    let c0: simd_float4 = [u[0], v[0], n[0], 0]\n    let c1: simd_float4 = [u[1], v[1], n[1], 0]\n    let c2: simd_float4 = [u[2], v[2], n[2], 0]\n\n    let v0 = simd_dot(-1*u, ev)\n    let v1 = simd_dot(-1*v, ev)\n    let v2 = simd_dot(-1*n, ev)\n    let c3: simd_float4 = [v0, v1, v2, 1]\n\n    let m: simd_float4x4 = simd_float4x4(columns: (c0, c1, c2, c3))\n    return m\n}\n\nfunc simd_make_frustum_float4x4(frustum: Float, aspectRatio: Float) -> simd_float4x4 {\n    // https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml\n    let left    = -frustum\n    let right   = frustum\n    let bottom  = -frustum/aspectRatio\n    let top     = frustum/aspectRatio\n    let near     = PanoramaView.Z_NEAR\n    let far      = PanoramaView.Z_FAR\n\n    let m00 = (2.0 * near) / (right - left)\n\n    let m11 = (2.0 * near) / (top - bottom)\n\n    let m20 = (right + left) / (right - left)\n    let m21 = (top + bottom) / (top - bottom)\n    let m22 = -1 * (far + near) / (far - near)\n    let m23 = Float(-1)\n\n    let m32 = -1 * (2 * far * near) / (far - near)\n\n    let c0: simd_float4 = [m00, 0, 0, 0]\n    let c1: simd_float4 = [0, m11, 0, 0]\n    let c2: simd_float4 = [m20, m21, m22, m23]\n    let c3: simd_float4 = [0, 0, m32, 0]\n    let m = simd_float4x4(columns: (c0, c1, c2, c3))\n\n    return m\n}\n\n// Translated from the original Panorama code\nfunc simd_make_quaternion_from_two_vectors(_ u: simd_float3, _ v: simd_float3) -> simd_quatf {\n    let w: simd_float3 = simd_cross(u, v)\n    var q: simd_quatf = simd_quaternion(w.x, w.y, w.z, simd_dot(u, v))\n    q.real += q.length\n    return q.normalized\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在 GLK 和 simd 之间来回转换

\n

这些函数可以在前面提到的 Panorama 存储库的GLK-Metal-Tools.swift文件中找到。如果按照建议您在控制器仅是simd后来回转换,则可以在慢慢删除GLK时将它们放在您的视图中

\n
func glkV3_to_simd(_ v3: GLKVector3) -> simd_float3 {\n    let v: simd_float3 = simd_make_float3(v3.x, v3.y, v3.z)\n    return v\n}\nfunc simd3_to_glk(_ v3: simd_float3) -> GLKVector3 {\n    let v = GLKVector3Make(v3[0], v3[1], v3[2])\n    return v\n}\n\nfunc glkV4_to_simd(_ v3: GLKVector4) -> simd_float4 {\n    let v: simd_float4 = simd_make_float4(v3.x, v3.y, v3.z, v3.w)\n    return v\n}\n\nfunc simd4x4_to_glk(_ m: simd_float4x4) -> GLKMatrix4 {\n    var array: [GLKVector4] = []\n    for i in 0..<4 {\n        let fv: simd_float4 = m[i]\n        let v: GLKVector4 = GLKVector4Make(fv[0], fv[1], fv[2], fv[3]);\n        array.append(v)\n    }\n    let mg: GLKMatrix4 = GLKMatrix4MakeWithColumns(array[0], array[1], array[2], array[3]);\n    return mg;\n}\nfunc glkm4_to_simd(_ m: GLKMatrix4) -> simd_float4x4 {\n    var array: [simd_float4] = []\n    for i in 0..<4 {\n        let fv: GLKVector4 = GLKMatrix4GetColumn(m, Int32(i))\n        let v: simd_float4 = simd_make_float4(fv[0], fv[1], fv[2], fv[3]);\n        array.append(v)\n    }\n    let ms: simd_float4x4 = simd_matrix(array[0], array[1], array[2], array[3]);\n    return ms;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我在开发过程中使用这些打印例程来检查各种值,您可能会发现它们也很有用:

\n
func print4x4SIMD(\n    msg: String,\n    m: simd_float4x4\n) {\n    var s = ""\n    s += "---COL: \\(msg)\\n"\n\n    let (c0, c1, c2, c3) = m.columns\n\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 0, c0[0], c0[1], c0[2], c0[3])\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 1, c1[0], c1[1], c1[2], c1[3])\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 2, c2[0], c2[1], c2[2], c2[3])\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 3, c3[0], c3[1], c3[2], c3[3])\n\n    print("\\n\\(s)\\n")\n}\nfunc print4x4GLK(\n    msg: String,\n    m: GLKMatrix4\n) {\n    var s = ""\n    s += "---COL: \\(msg)\\n"\n\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 0, m.m00, m.m01, m.m02, m.m03)\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 1, m.m10, m.m11, m.m12, m.m13)\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 2, m.m20, m.m21, m.m22, m.m23)\n    s += String(format: "[%.2d] %10.4lf %10.4lf %10.4lf %10.4lf\\n", 3, m.m30, m.m31, m.m32, m.m33)\n\n    print("\\n\\(s)\\n")\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n

simd_float4x4 旋转

\n

虽然我还没有使用过这个,但有一天我可能会需要它(它未经测试):

\n
func matrix_from_rotation(radians: Float, v _v: simd_float3) -> simd_float4x4 {\n    // https://www.haroldserrano.com/blog/rotating-a-2d-object-using-metal\n    let v: simd_float3 = simd_normalize(_v)\n    let cos: Float = cos(radians)\n    let cosp: Float = 1 - cos\n    let sin: Float = sin(radians)\n\n    let col0 = simd_float4(\n        cos + cosp * v.x * v.x,\n        cosp * v.x * v.y + v.z * sin,\n        cosp * v.x * v.z - v.y * sin,\n        0\n    )\n\n    let col1 = simd_float4(\n        cosp * v.x * v.y - v.z * sin,\n        cos + cosp * v.y * v.y,\n        cosp * v.y * v.z + v.x * sin,\n        0.0\n    )\n\n    let col2 = simd_float4(\n        cosp * v.x * v.z + v.y * sin,\n        cosp * v.y * v.z - v.x * sin,\n        cos + cosp * v.z * v.z,\n        0.0\n    )\n\n    let col3 = simd_float4(0, 0, 0, 1)\n\n    let m: simd_float4x4 = simd_float4x4(columns: (col0, col1, col2, col3))\n    return m\n}\n
Run Code Online (Sandbox Code Playgroud)\n

结论

\n

这个项目花了我大约半年的时间,每周工作一个周末,但它却持续了 18 个月。原因是:我花了很多天没有任何进展,得到奇怪的损坏的输出,或者没有输出,当我最终得到在Metal中显示的主要视图时,我把项目收起来了。我太累了,无法继续下去。

\n

也就是说,iOS 的末日即将结束,而且随着岁月的流逝,那个末日也越来越近了。

\n

当我让Metal处理 GLK 向量和矩阵时,我原本打算停止,但 Warren Moore 敦促我现在转换为simd

\n

当我终于构建了公司的应用程序并且没有与GLKit相关的单个编译器警告时,那是一个纯粹的狂喜时刻!

\n