読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

SceneKitを触ってみた

iOS Swift OpenCV Objective-C

とある事情にて3Dグラフィックスを扱う方法を調べていたところ、SceneKitにたどり着きました。

dae形式の3Dオブジェクトを表示するアプリが作れるようです。

というわけで、早速iOSで触ってみたのでメモを残しておきます。

PutYourRiceCakes - github

やったこと

  1. Blenderで3Dオブジェクトを作って、dae形式で出力
  2. Xcodeでプロジェクトを作成し、1のオブジェクトをインポートする
  3. UIImageViewをStoryboardに追加して、カメラの映像を表示する
  4. 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っぽいアプリができるかな?というところです。

この辺りを(私の気が変わらなければ)大晦日ハッカソンで挑戦してみようと思います。

参考

Blender

SceneKit

UIColor