SceneKit - 从 SCNView 获取渲染场景作为 MTLTexture 而不使用单独的 SCNRenderer

Posted

技术标签:

【中文标题】SceneKit - 从 SCNView 获取渲染场景作为 MTLTexture 而不使用单独的 SCNRenderer【英文标题】:SceneKit - Get the rendered scene from a SCNView as a MTLTexture without using a separate SCNRenderer 【发布时间】:2017-03-10 20:00:43 【问题描述】:

我的 SCNView 使用 Metal 作为渲染 API,我想知道是否有一种方法可以将渲染的场景作为 MTLTexture 来获取,而无需使用单独的SCNRenderer?当我尝试通过 SCNView 显示场景并通过 SCNRenderer 将屏幕外场景重新渲染到 MTLTexture 时,性能下降(我正在尝试获取每一帧的输出)。

SCNView 允许我访问它使用的 MTLDeviceMTLRenderCommandEncoderMTLCommandQueue,但不能访问为了获得 MTLTexture(通过renderPassDescriptor.colorAttachments[0].texture

,我需要底层的 MTLRenderPassDescriptor

我尝试过的一些替代方法是尝试使用 SCNView.snapshot() 获取 UIImage 并对其进行转换,但性能更差。

【问题讨论】:

根据这里所说的developer.apple.com/reference/metal/…,您需要创建一个新的 MTLRenderPassDescriptor,获取 MTLRenderPassAttachmentDescriptor 并将 MTLTexture 设置为您的渲染目标。鉴于您有可用的东西,例如 MTLRenderCommandEncoder,似乎他们让您可以绘制,而不是从中获取缓冲区。如果您可以创建自己的 MTLRenderPassDescriptor 并将其提供给 SCNView,您可以设置渲染目标。 哦,看起来您可以使用 SCNRenderer developer.apple.com/reference/scenekit/scnrenderer 设置您自己的 MTLRenderPassDescriptor,这可能就是您需要做的。 设置 SCNRenderer 会导致它重新渲染场景(因为它在自己的渲染周期中),从而导致性能下降。我希望只从已经渲染的场景中获取底层纹理(假设 SCNView 渲染成一个,这似乎是这种情况,因为它使用 CAMetalLayer 作为视图的支持层)。 你是在做镜子吗? 我在想这将是一个不反射的非常奇怪的镜子,纳米!不过我很好奇你是如何解决这个问题的。 【参考方案1】:

为 Swift 4 更新:

Swift 4 不支持 dispatch_once(),并且 @objc 添加到替换函数中。这是更新的调酒设置。经过测试,这对我来说效果很好。

extension CAMetalLayer 

    // Interface so user can grab this drawable at any time
    private struct nextDrawableExtPropertyData 
        static var _currentSceneDrawable : CAMetalDrawable? = nil
    
    var currentSceneDrawable : CAMetalDrawable? 
        get 
            return nextDrawableExtPropertyData._currentSceneDrawable
        
    

    // The rest of this is just swizzling
    private static let doJustOnce : Any? = 
        print ("***** Doing the doJustOnce *****")
        CAMetalLayer.setupSwizzling()

        return nil
    ()

    public static func enableNextDrawableSwizzle() 
        _ = CAMetalLayer.doJustOnce
    

    public static func setupSwizzling() 
        print ("***** Doing the setupSwizzling *****")

        let copiedOriginalSelector = #selector(CAMetalLayer.originalNextDrawable)
        let originalSelector = #selector(CAMetalLayer.nextDrawable)
        let swizzledSelector = #selector(CAMetalLayer.newNextDrawable)

        let copiedOriginalMethod = class_getInstanceMethod(self, copiedOriginalSelector)
        let originalMethod = class_getInstanceMethod(self, originalSelector)
        let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

        let oldImp = method_getImplementation(originalMethod!)
        method_setImplementation(copiedOriginalMethod!, oldImp)

        let newImp = method_getImplementation(swizzledMethod!)
        method_setImplementation(originalMethod!, newImp)

    


    @objc func newNextDrawable() -> CAMetalDrawable? 
        // After swizzling, originalNextDrawable() actually calls the real nextDrawable()
        let drawable = originalNextDrawable()

        // Save the drawable
        nextDrawableExtPropertyData._currentSceneDrawable = drawable

        return drawable
    

    @objc func originalNextDrawable() -> CAMetalDrawable? 
        // This is just a placeholder. Implementation will be replaced with nextDrawable.
        // ***** This will never be called *****
        return nil
    

在您的 AppDelegate 中:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool 
    // Override point for customization after application launch.

    // Swizzle
    CAMetalLayer.enableNextDrawableSwizzle()

    return true

更新为向 CAMetalLayer 添加 currentSceneDrawable 属性,因此您只需使用 layer.currentSceneDrawable 即可访问它,而不是让扩展程序将其存储在外部。

【讨论】:

【参考方案2】:

** 警告:这可能不是 App Store 的正确方法。但它正在工作。

第1步:使用swizzling将CAMetalLayer的nextDrawable方法换成新的。 为每个渲染循环保存 CAMetalDrawable。

extension CAMetalLayer 
  public static func setupSwizzling() 
    struct Static 
      static var token: dispatch_once_t = 0
    

    dispatch_once(&Static.token) 
      let copiedOriginalSelector = #selector(CAMetalLayer.orginalNextDrawable)
      let originalSelector = #selector(CAMetalLayer.nextDrawable)
      let swizzledSelector = #selector(CAMetalLayer.newNextDrawable)

      let copiedOriginalMethod = class_getInstanceMethod(self, copiedOriginalSelector)
      let originalMethod = class_getInstanceMethod(self, originalSelector)
      let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

      let oldImp = method_getImplementation(originalMethod)
      method_setImplementation(copiedOriginalMethod, oldImp)
      method_exchangeImplementations(originalMethod, swizzledMethod)
    
  


  func newNextDrawable() -> CAMetalDrawable? 
    let drawable = orginalNextDrawable()
    // Save the drawable to any where you want
    AppManager.sharedInstance.currentSceneDrawable = drawable
    return drawable
  

  func orginalNextDrawable() -> CAMetalDrawable? 
    // This is just a placeholder. Implementation will be replaced with nextDrawable.
    return nil
  

第 2 步: 在 AppDelegate 中设置 swizzling:didFinishLaunchingWithOptions

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool 
  CAMetalLayer.setupSwizzling()
  return true

第 3 步: 为您的 SCNView 的 CAMetalLayer 禁用 framebufferOnly(以便为 MTLTexture 调用 getBytes)

if let metalLayer = scnView.layer as? CAMetalLayer 
  metalLayer.framebufferOnly = false

第 4 步: 在您的 SCNView 的委托 (SCNSceneRendererDelegate) 中,使用纹理

func renderer(renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: NSTimeInterval) 
    if let texture = AppManager.sharedInstance.currentSceneDrawable?.texture where !texture.framebufferOnly 
      AppManager.sharedInstance.currentSceneDrawable = nil
      // Play with the texture
    

第 5 步(可选): 您可能需要在 CAMetalLayer 确认您获得的可绘制对象是您的目标。 (如果同时有多个 CAMetalLayer)

【讨论】:

我什至没有想到!非常聪明!谢谢你:) @KevinBui 你在你的应用程序上试过这个吗?如果是这样,App Store 是否有任何问题? @halileohalilei 我在 App Store 中发布了一个应用程序,审核没有任何问题。 itunes.apple.com/us/app/facemous/id1213785099?mt=8

以上是关于SceneKit - 从 SCNView 获取渲染场景作为 MTLTexture 而不使用单独的 SCNRenderer的主要内容,如果未能解决你的问题,请参考以下文章

SceneKit:在 SCNView 上渲染 SpriteKit 粒子系统时应用程序崩溃,当所有代码似乎都是系统代码的一部分时如何调试

SCNView第一次渲染场景很慢

SceneKit开发教程02 SCNScene和SCNView

无法使用 SceneKit 从 OpenGL 获取 CVPixelBufferRef 数据

iOS 12 中autoenablesDefaultLighting 太亮,SCNView.pointOfView 无效

DefaultCameraController 上的 SceneKit 动画