SceneKitを触ってみた
とある事情にて3Dグラフィックスを扱う方法を調べていたところ、SceneKitにたどり着きました。
dae形式の3Dオブジェクトを表示するアプリが作れるようです。
というわけで、早速iOSで触ってみたのでメモを残しておきます。
やったこと
- Blenderで3Dオブジェクトを作って、dae形式で出力
- Xcodeでプロジェクトを作成し、1のオブジェクトをインポートする
- UIImageViewをStoryboardに追加して、カメラの映像を表示する
- SceneKitViewを追加してインポートした1のオブジェクトを表示できるようにする
1.Blenderで3Dオブジェクトを作って、dae形式で出力
BlenderでもMayaでもお好きなソフトで、表示する3Dオブジェクトを作成します。
この時注意が必要なのは、マテリアルは必ずそれぞれのオブジェクトにセットしてからExportすることです。
セットしていないと、Xcodeでインポートした時に問題が発生します...。
2.Xcodeでプロジェクトを作成し、1のオブジェクトをインポートする
プロジェクトの作成
[Single View Application]でプロジェクトを作成しました。
SceneKitを使う場合、[Game]を選んで[Game Technology]で[SceneKit]を選択するのが楽ではあります(いきなり飛行機の3Dオブジェクトが表示される状態でプロジェクトが作成されます)。
しかし、この後行うStoryboardへの[UIImageView]と[SceneKitView]の2つのViewを追加することができなかったため、[Single View〜]を選択しています。
3Dオブジェクトのインポート
インポートはNavigatorに直接ドラッグ&ドロップでも、メニューからでもOKです。
テクスチャーをセットする場合はそれらもインポートしておきます。
3Dオブジェクトの設定
Navigatorでインポートしたdaeファイルをクリックすると、3Dオブジェクトの編集画面が表示されます。
ここで各オブジェクトの座標値や回転、テクスチャの割り当てなどを行うことができます。
テクスチャを割り当てるのも、対象のオブジェクトを選択した状態でテクスチャをドラッグ&ドロップすればOKですが、あらかじめオブジェクトにマテリアルが設定されていないとXcodeがクラッシュします。
Xcodeがクラッシュまでするのはバグでしょうから、そのうち治るかとは思いますが。
3.UIImageViewをStoryboardに追加して、カメラの映像を表示する
ViewController.swift側は以前と同じで、AVFoundationを使ってカメラの映像をSampleBufferから取得します。
SampleBufferからcv::Mat形式で画像を取得 -> UIImageに変換するところは、今回グレイスケールにしたり輪郭をとったりはしないので、その部分がシンプルになっています。
ImageController.mm
#import "PutYourRiceCakes-Bridging-Header.h" #import < opencv2 / opencv.hpp > #import "opencv2 / imgcodecs / ios.h" @interface ImageController() @property (nonatomic)CVImageBufferRef ibrImageBuffer; @property (nonatomic)CGColorSpaceRef csrColorSpace; @property (nonatomic)uint8_t *baseAddress; @property (nonatomic)size_t sztBytesPerRow; @property (nonatomic)size_t sztWidth; @property (nonatomic)size_t sztHeight; @property (nonatomic)CGContextRef cnrContext; @property (nonatomic)CGImageRef imrImage; @property (nonatomic, strong)UIImage *imgCreatedImage; @property (nonatomic, strong)UIImage *imgGray; @property (nonatomic) cv::Scalar sclLineColor; @end @implementation ImageController - (void) initImageController { _sclLineColor = cv::Scalar(255, 255, 255); } - (UIImage *) createImageFromBuffer:(CMSampleBufferRef) sbrBuffer { _ibrImageBuffer = CMSampleBufferGetImageBuffer(sbrBuffer); // ピクセルバッファのベースアドレスをロックする. CVPixelBufferLockBaseAddress(_ibrImageBuffer, 0); // ベースアドレスの取得. _baseAddress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(_ibrImageBuffer, 0); // サイズの取得. _sztWidth = CVPixelBufferGetWidth(_ibrImageBuffer); _sztHeight = CVPixelBufferGetHeight(_ibrImageBuffer); cv::Mat matCamera((int)_sztHeight, (int)_sztWidth, CV_8UC4, (void*)_baseAddress); // 90°回転. cv::transpose(matCamera, matCamera); // 左右反転. cv::flip(matCamera, matCamera, 1); // カラーの指定. cv::cvtColor(matCamera, matCamera, cv::COLOR_RGBA2BGRA); // cv::matからUIImageに変換. _imgCreatedImage = MatToUIImage(matCamera); matCamera.release(); // ベースアドレスのロックを解除 CVPixelBufferUnlockBaseAddress(_ibrImageBuffer, 0); return _imgCreatedImage; } @end
- カラーの指定をしないと映像が青くなってしまうので(それはそれで面白いですが)、cv::COLOR_RGBA2BGRAを指定しています。
4.SceneKitViewを追加してインポートした1のオブジェクトを表示できるようにする
まずSceneKitViewを、Storyboard上で3のUIImageViewに重なるように追加します。
この時SceneKitViewが手前にセットされている必要があります。
コードですが、プロジェクト作成で[Game]を選んで[Game Technology]で[SceneKit]を指定した場合に作成されるものとほぼ同じです。
fpsなどの情報は不要なため、その辺りは削除しましたが...。
ViewController.swift
import SceneKit class ViewController: UIViewController { @IBOutlet weak var scvRiceCakeView: SCNView! override func viewDidLoad() { self.initSceneView() } func initSceneView() { // シーンを作り、daeファイルを読み込む. let scnRiceCake = SCNScene(named: "RiceCake.dae")! // オブジェクトを映すカメラを作り、シーンに追加する. let nodCamera = SCNNode() nodCamera.camera = SCNCamera() scnRiceCake.rootNode.addChildNode(nodCamera) // カメラの位置を指定する. nodCamera.position = SCNVector3(x: 0, y: 0, z: 15) // ライトを作成し、シーンに追加する. let nodLight = SCNNode() nodLight.light = SCNLight() // ライトの種類を指定する(全方位照射). nodLight.light!.type = SCNLightTypeOmni nodLight.position = SCNVector3(x: 0, y: 10, z: 10) nodLight.light!.color = UIColor.whiteColor() scnRiceCake.rootNode.addChildNode(nodLight) // 環境光を作成し、シーンに追加する. let nodAmbiendLight = SCNNode() nodAmbiendLight.light = SCNLight() nodAmbiendLight.light!.type = SCNLightTypeAmbient nodAmbiendLight.light!.color = UIColor.darkGrayColor() scnRiceCake.rootNode.addChildNode(nodAmbiendLight) scvRiceCakeView.scene = scnRiceCake // カメラ操作を有効にする. scvRiceCakeView.allowsCameraControl = true // 背景色の設定(透明にする). scvRiceCakeView.backgroundColor = UIColor(white:1, alpha:0) } }
- SCNLightの種類にはDirectionalなどもありますが、その場合はnodLight.rotation(SCNVector4)で方向を指定すると良さそうです。
- カメラ操作を有効にすることで、画面タッチでオブジェクトを回転させたり、ピンチイン・ピンチアウトの操作ができるようになります。
あとはオブジェクトの表示位置や大きさを、OpenCVを使って設定できるとARっぽいアプリができるかな?というところです。
この辺りを(私の気が変わらなければ)大晦日ハッカソンで挑戦してみようと思います。