てなわけで、無事ライブ映像は表示されたっすか〜?
まあでも、ライブ映像が表示されるだけじゃねぇ…、AVCaptureSessionを使えば、ライブ映像加工程度は扱えるわけだしね。
あれだけじゃあ、とてもARKitのサンプルだよとは言えませんな。
ぜひともライブ映像に3D仮想体を合成せねば。
サンプル:AVCaptureSessionを使ったライブ映像のセピア色加工
http://tetera.jp/xcc/book-sample/avcam.zip
実機で動かす時はTeamを自分のApple IDにしてね
注意)本来、もうちょっと設定することがあるんだけど、できるだけシンプルに済ましてる。そのため加工した映像は正しい縦横比にならず向きも正しくない。加工自体も非同期処理にすべきだけど、Core Imageフィルターがそこそこ速いのでメインスレッドで実行してる。興味がある人はViewController.swiftソースに出てくるクラスやメソッドを調べてみましょう。
といってもARSession自体も、ライブ映像の取り込み自体は、内部で上のサンプルのようにAVCaptureSession使ってるんじゃないかとは思うわけですよ。その上でライブ映像と3D仮想体を描いた画像(以後CGI画像て呼ぶ)とを合成してるんじゃないかと。
注意)CGI:Computer Generated Imagery
ちなみにARでのCGI画像は次のようなステップをふんで生成されるわけですが…
1、3D仮想空間を用意
2、用意した仮想空間に仮想体を配置
3、同様に仮想のカメラを配置
4、仮想カメラの向いた先の空間が、レンズを通してどのように見えるか計算
この仮想カメラの位置や向きを、iPhone実機のカメラの位置や向きと常時連動させることでARが実現されるわけすね。
それをやってくれるのがARSessionとARSCNViewてわけで、連動するための取り決めとして
1、実際の空間も、仮想空間もARセッション開始時のiPhoneの位置を原点とする
2、ARセッション開始時の仮想カメラは原点に配置する
としておいて、あとはセッション中、実際の空間で開始時に決めた原点からのiPhoneのズレを加速度センサ等で測定し、仮想空間の仮想カメラの位置に反映させ続ける、と同時に、実機の向きをジャイロで測定し、仮想カメラの向きも連動させてるわけです。
なので、ライブ画像に仮想体を表示するために、こっちがやらないとダメな残りの作業は、仮想体を配置した3D仮想空間の提供ってことになるわけっす。
それをやってるのが、前々回提供したARサンプルの残りの部分てわけ。
前々回のARサンプル:
http://tetera.jp/xcc/book-sample/ar.zip
実機で動かす時はTeamを自分のApple IDにしてね
class ViewController: UIViewController { ・・・ override func viewDidLoad() { super.viewDidLoad() ・・・ // 3D文字列を管理する仮想シーン環境 let scene = SCNScene() // 3D文字列(こんにちはAR)の作成と仮想シーン環境への登録 let geometry = SCNText(string: "こんにちはAR", extrusionDepth: 3) let node = SCNNode(geometry:geometry) // 横の大きさを50cmにする let scale = 1 / (geometry.boundingSphere.radius * 2) * 0.5 node.scale = SCNVector3(x:scale, y:scale, z:scale) // 配置と登録 前方50cmの空中に配置 node.position = SCNVector3(-0.25, 0.1, -0.5) scene.rootNode.addChildNode(node) // ARシーンを表示するビューへの仮想シーン環境の登録 sceneView.scene = scene }
まず、最初に作成してるSCNSceneってのは、3D仮想空間全体を管理するオブジェクトっす。
let scene = SCNScene()
ARSCNViewは、このオブジェクトが表現する仮想空間内に自分が用意した仮想カメラを配置して、カメラに映り込む画像をCGI画像として作り出すわけですな。
SCNScene
で、こいつは自身の3D仮想空間や、その中に配置された仮想体群をSCNNodeというオブジェクトとして管理してます。
SCNNode
例えば、3D仮想空間に机が置かれて、その上に本が置かれているとしましょう。その場合、SCNSceneは以下の3つのSCNNodeを持つことになる。
1、自身が持つ3D仮想空間を表現したSCNNode(以後、ルートノードと呼ぶ)
2、机を表現したSCNNode(以後、机ノードと呼ぶ)
3、本を表現したSCNNode(以後、本机ノードと呼ぶ)
で、こいつらSCNNodeは、上の図でも示してるように、それぞれが独自の3D仮想空間と原点を持つことになっていて、互いに親子関係を結べるんですな。そうすることで、子側の仮想空間の原点位置やx、y、z軸の向きを、親側の空間座標から相対的に指定できるようになってます。
なので、仮想空間があって、その中に机が置かれ、机の上には本が置かれているというなら、上の3つのSCNNodeは、一般的に次のような親子関係にします。
もちろん、ルートノードの子ノードとして、机と本ノードを並べることもできるんだけど…
机の上に本が置かれているなら、本ノードは、ルートノードじゃなく、机ノードの仮想空間相対で子ノードの原点位置や軸の向きを指定するのが正解っす。そうしておくと、机ノードを回転させるだけで、子側の本ノードも一緒に回転するようになるから。
ま、ここらへんの親子関係を組める恩恵は、ロボットアームなんかを動かす時に確かめていきましょう。
↓でっかいアームとちっちゃいアーム
とりあえず、今回は仮想体として3D文字列を1つ作って、SCNSceneの管理する3D仮想空間であるルートノードの子ノードにしたいわけです。
で、この3D文字列をノードとして作ってるのが
let geometry = SCNText(string: "こんにちはAR", extrusionDepth: 3) let node = SCNNode(geometry:geometry)
てところ。SCNodeオブジェクトを作る時に、引数としてSCNTextオブジェクトを渡してるのがポイントですな。
SCNText
こいつが3D文字列の形状を表現したオブジェクトっす。
作成時は、引数stringで作成したい文字列を指定し、引数extrusionDepthでその厚みを指定します。
SCNText(string: "こんにちはAR", extrusionDepth: 3)
オブジェクトの中身は3D頂点群と、その頂点を結んで構成される三角形平面の集まりっす。
というのも現在のPCやスマホで描かれるあらゆる3D形状は、三角形平面の集まりで表現されてるからなんですな。球体なんかも三角形平面の集まりで表現します。
3D形状は三角形平面の集まりでしか表現できないってわけじゃないけど、三角形平面の集まりで表現すると描画処理が単純になって便利なんですよ。
3Dキャラクタも結局三角形平面の集まりで表現する。
注意)コンピュータで、3Dの頂点情報から2D画像を描く方法について興味あるなら「iPhoneアプリ開発、その(90) 自力でOpenGLしてみる」あたりを読んでみましょう。
三角形平面の集まりで3D形状を表現するのが、いわゆる業界標準になってるのでARKitもそれに従ってます。
スキン/メッシュ
一般的には、この三角形平面の集まりで表現された3D形状のことを、メッシュ(mesh:網目)とかスキン(skin:皮、肌)なんて呼んだりする。網目はなんとなく想像できるけど、なんでスキン?てのは3Dキャラクタを動かす段階で納得するで正太郎。
で、ARKitではこのメッシュを管理するオブジェクトをSCNGeometryクラスとして定義してます。
SCNGeometry
SCNTextはこのクラスの派生なわけですな。3D文字列の他にもいくつか基本の3D形状が用意されてるけど…
まあ、あんま使わないっすね。基本、Blenderといった3Dモデラーを使って用意したものを使います。
注意)Blenderについてはその昔、「iPhoneアプリ開発、その(117) SIO2でいいじゃん」でちょっと触れた。SIO2はUnityに取って代わられたけどBlenderは今でも元気ですな。
ここらへんは、ロボットアームを油圧ショベルの変えるときに説明。ビフォア、アフター的な…
ちなみに、ARKitの3D空間座標系の単位はメートルです。なので引数で指定してる文字列の厚みは3mてことになる。
でかくね?
と思った人もいると思うけど、実際でかいです。
今回の「こんにちはAR」の横幅で70mくらい。
そのまま配置するとクッソでかい文字列が表示されるんですな。何故にそんなにでかいのって思うかもしれないけど、多分理由はないです。
2D画像として、フォントサイズに10ポイントを指定して「こんにちはAR」を描くと、横幅70ポイントくらいになるんで、これをそのままメートル単位として使ってるんじゃないかと思われ。勘だけど。
3D文字列作成時に、Core Text使って各文字のグリフをベジェ曲線で取り出して、三角形平面の頂点を計算してるんじゃないかと…
注意)「Core Text使って各文字のグリフをベジェ曲線…」に興味がある人は「iPhoneアプリ開発、その(240) ヒエログリフのグリフ」を読んでみましょう。
配置する時はメッシュが存在するノードの3D仮想空間を適当にスケーリングできるので、メッシュ自体の大きさは問題ないんですよ。
ノードが持つ3D仮想空間のスケーリング
ノードが持つ3D仮想空間のスケーリングとは、親ノード側の空間から見た場合のことです。
例えば、親子関係を結んだ机ノードと本ノードで、本ノードのメッシュが2mくらいで定義されてて、そのままだと机に置けない場合、子供である本ノード側の3D仮想空間をスケーリングすることで、机に置けるサイズにできるんですな。
この時、変わるのは親ノード側の3D仮想空間からみた子ノード側の3D仮想空間の大きさであって、子ノード側3D仮想空間内部の座標系はなんら変化してない点に注目。
なので、子ノードのメッシュ(この例だと本の形状を定義する頂点座標群)の座標データは何もいじらなくていい。本人はあくまで2mの本としてメッシュを定義しとけばいいわけですよ。
実際、私は以下の作業でノードの持つ3D仮想空間をスケーリングして、表示される3D文字列の横幅を50cmにしてるわけです。
let scale = 1 / (geometry.boundingSphere.radius * 2) * 0.5 node.scale = SCNVector3(x:scale, y:scale, z:scale)
ここで参照してるSCNTextのboundingSphereプロパティには、3D文字列メッシュ全体を覆う最小の球体が設定されてて、その半径がradiusプロパティから取り出せるようになってます。
なので、この値を2倍すれば球体の直径、つまり「こんにちはAR」メッシュの幅が求まるわけです。で、その逆数をとることで、「こんにちはAR」メッシュの幅を1mにするための3D仮想空間のスケーリング比が求まる。
1 / 「こんにちはAR」メッシュの幅
このスケーリング比を、そのまま3D文字列ノード(SCNNode)のscaleプロパティにSCNVector3として指定すれば、3D文字ノードが持つ3D仮想空間がスケーリングされ、親ノード側の3D仮想空間での3D文字列の幅は1mになる。私は0.5を追加でかけて0.5mにしてます。
SCNVector3
こいつは3次元ベクトルの要素であるx、y、zをひとまとめにした構造体。CGPointの3次元版と思ったらいいです。x、y、z均等にスケーリングさせるために全部同じ値にしてる。
最後に、この3D文字列ノードを、親ノードであるSCNSceneの3D仮想空間のどこに置くかだけど、先に書いたようにARセッション開始時は仮想カメラが原点に置かれることになってんですよ。
なので、3D文字列はそのカメラの手前にくるように配置してます。それが3D文字列ノード(SCNNode)のpositionプロパティへのSCNVector3の設定。x軸上で-0.25m, y軸上で0.1m, z軸上で-0.5mのところに3D文字列ノードが持つ仮想空間の原点がくるように指定してる。
node.position = SCNVector3(-0.25, 0.1, -0.5)
3D文字列ノードが持つ仮想空間の原点は、文字列の左下あたりです。
この原点が、親側の仮想空間の座標(-0.25m, 0.1m, -0.5m)にくるように配置ってわけです。
ちなみにARKitの3D座標系は右手系というやつです。これを把握せずに座標扱うとトラブルので注意しましょう。z軸の進行方向が逆になるんですな。
注意)OpenGLやMetalの3D座標系も右手系。Direct Xは左手系(右手系に切り替え可能)っす。
SCNSceneの管理する3D仮想空間はSCNNodeとして作成され、SCNSceneのrootNodeプロパティに設定済みなので、このノードにaddChildNodeメッセージで3D文字列ノードを子ノードとして追加すれば、SCNSceneの管理する3D仮想空間に3D文字列という仮想体が配置されることになります。
scene.rootNode.addChildNode(node)
これで3D仮想空間の準備ができたので、こいつを使ってねとARSCNViewにしらせれば作業はおしまい。
これはARSCNViewのsceneプロパティにSCNSceneを設定することで行います。
sceneView.scene = scene
これでARセッションが始まると、ARSCNViewは用意した仮想カメラを、sceneプロパティに設定されたSCNSceneが持つ3D仮想空間に配置して、CGI画像を作り出しライブ映像に合成していくことになる。
サンプルのソースをいじり、スケーリングや配置位置を変えて色々試してみてね〜。
じゃまた。