SceneKit と Metal で片方のビューをキャプチャし、他のビューのジオメトリに貼り付ける
SCNView の snapshot() メソッドでシーン自体のスクリーンショットを取得できる。
スレッドセーフでいつでも呼べるとはドキュメントには書いてあるが処理時間がかかるので連続で呼ぶとフレームが落ちる。
そのため、別の方法を使い Metal の命令から SceneKit のシーンのレンダリング結果を取得する方法を試してみる。
今回の記事の注意点としては、 フレーム毎に Command Buffer を生成するため、そこまで処理が軽くない。
また、今回 Metal に関してはあまり解説しないため本や Web で調べて欲しい。
サンプルファイル
上もしくは左のビューを動かすと、もう片方のビューのテクスチャ画像も動く。
(テクスチャを正方形にしているので画面比 約 2:1 の端末が望ましい。)
流れ
- 2つの SCNView と SCNScene を設定
- グロバールで宣言している MTLTexture を設定し、ジオメトリに貼り付ける
- MTLTextureDescriptor からテクスチャを生成し、上記の MTLTexture を設定する
- どちらかの SCNView に SCNSceneRendererDelegate を設定してフレーム毎に Metal のレンダリング処理を上記の MTLTexture に渡す
コードの説明
デリゲート
SCNView を配置する ViewController に SCNSceneRendererDelegate を追加する。
class GameViewController: UIViewController, SCNSceneRendererDelegate { ... }
グローバル変数
IBOutlet の 2 つはシーンのキャプチャを撮るものと、貼り付けるジオメトリがあるシーンを設定。
capturedNode は貼り付けるジオメトリ。
@IBOutlet weak var captureView: ç! @IBOutlet weak var mainView: SCNView! // Box geometry node private var capturedNode:SCNNode!
Metal の設定
device はキャプチャする SCNView から MTLDevice を引っ張ってくるためのもの。
commandQueue はシーンからテクスチャを書き込むためのキューである MTLCommandQueue。
renderer は SCNRenderer の描画処理を MTLRenderPassDescriptor から MTLTexture へ書き込むためのもの
// Metal var device: MTLDevice! var commandQueue: MTLCommandQueue! var renderer: SCNRenderer!
offscreenTexture はキャプチャした画像を保存する MTLTexture。
rgbColorSpace、bytesPerPixel、bitsPerComponent、bitsPerPixel、textureSizeX、textureSizeY は変数名通り各種テクスチャ設定。
// Texture Settings var offscreenTexture: MTLTexture! let rgbColorSpace = CGColorSpaceCreateDeviceRGB() let bytesPerPixel = Int(4) let bitsPerComponent = Int(8) let bitsPerPixel:Int = 32 var textureSizeX:CGFloat! var textureSizeY:CGFloat!
viewDidLoad
MTLDevice、MTLCommandQueue、SCNRenderer の初期化をして、テクスチャを設定する際のサイズを決める。
そして、setupTexture() でテクスチャを設定をする。
// Metal Settings device = captureView.device commandQueue = device.makeCommandQueue() renderer = SCNRenderer(device: device, options: nil) // Texture Settings textureSizeX = captureView.bounds.width textureSizeY = captureView.bounds.height setupTexture()
“func renderer(_ renderer:, willRenderScene scene:, atTime time:) {}” を使用するため、
どちらかの SCNView で delegate を設定する。
mainView.delegate = self
setupTexture() 関数
textureSizeX、Y を使用してデータ幅、bytesPerRow で行毎のバイト、
texture2DDescriptor でピクセルフォーマットとサイズを設定。
とりあえず、ミップマップは使用しないので無視。
MTLTextureUsage は使用方法の設定。
レンダーパスでテクスチャにレンダリング、シェーダ内のテクスチャを読み取るかサンプリングするためのオプション。
region で テクスチャデータ用の2Dの矩形領域をを設定し replace メソッドで変更し texture を offscreenTexture に texture を渡す。
func setupTexture() { var rawData0 = [UInt8](repeating: 0, count: Int(textureSizeX) * Int(textureSizeY) * 4) let bytesPerRow = 4 * Int(textureSizeX) let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: MTLPixelFormat.rgba8Unorm, width: Int(textureSizeX), height: Int(textureSizeY), mipmapped: false) textureDescriptor.usage = MTLTextureUsage(rawValue: MTLTextureUsage.renderTarget.rawValue | MTLTextureUsage.shaderRead.rawValue) let texture = device.makeTexture(descriptor: textureDescriptor) let region = MTLRegionMake2D(0, 0, Int(textureSizeX), Int(textureSizeY)) texture?.replace(region: region, mipmapLevel: 0, withBytes: &rawData0, bytesPerRow: Int(bytesPerRow)) offscreenTexture = texture }
update 関数
デリゲートのメソッドを設定し sceneRender() を呼び出す。 willRenderScene より didRenderScene 処理が速くなると思われる。
内部で ceneRender() 関数を呼び出している。
// MARK: - SceneKit Delegate func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { sceneRender() }
renderer 関数
viewport でサイズを指定し、renderPassDescriptor の colorAttachments の texture に offscreenTexture を設定。
SCNView の Command Queue から Command Buffer を作成。
renderer.scene に SCNView の scene、
renderer.pointOfView に SCNView の pointOfView を設定。
render で上記のものを設定し、Command Buffer を commit してテクスチャの処理を開始する。
// MARK: - Metal func sceneRender() { let viewport = CGRect(x: 0, y: 0, width: CGFloat(textureSizeX), height: CGFloat(textureSizeY)) let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = offscreenTexture let commandBuffer = commandQueue.makeCommandBuffer() // reuse scene1 and the current point of view renderer.scene = captureView.scene renderer.pointOfView = captureView.pointOfView renderer.render(atTime: 0, viewport: viewport, commandBuffer: commandBuffer!, passDescriptor: renderPassDescriptor) commandBuffer?.commit() }
まとめ
本来 MTLRenderPassDescriptor を使用するケースはシェーダーにテクスチャとして渡し処理する場合だろう。
Command Buffer を使いまくるので、特に利用する機会はないが、このようなことができるよといった感じ。