iPhoneアプリ開発:AR 目次

 

 というわけで、今回からしばらく物理の時間っす。

 ちなみに物理学は英語で

Physics

 と書いてフィジックスと呼ぶのだよ。いまだに綴りを間違っちゃう。

 

 私達もついに、現実世界の画面と3D仮想体をシンクロさせるだけじゃなく、仮想体に現実世界の物理運動を再現させるという、ARの新たな領域に踏み込むわけだが…

 いったいどうしろと?

 こういう時は、developer.apple.comを、キーワード「physics」あたりで検索すればよくってよ。

注意)ここら辺のノウハウは「リブートキャンプ by Swift 目次」の「12、または私は如何にして心配するのを止めて…」で説明してるんで、わからん人はそっち読むように

 

https://developer.apple.com/search/?q=physics&type=Videos

 

 検索結果いっぱい出ます。

 

 

 このうち、VideosタブてっぺんのInside SwiftShot: Creating an AR Gameなんかは、WWDC 2018で公開されたAR対戦ゲームで使われてる技術の説明で、ソースまで提供されとります。

 複数のiPhone実機間での実空間の共有や、そのための実機間通信、シェーダを使った旗を揺らす方法、そういった手法のとっかかりを紹介してます。

 具体的な手法を知るためには提供されてるプロジェクトを読み込まないとダメなんだけど、やれば、かなりの情報が手に入りそう。

 これまでの話を読んでる人なら、このサンプル使って自力でなんとかできるかもしれんけど、敷居は高めです。

 私は、とりあえずは、地道に進んでみようかと思ってます。

 なら、どっから手をつけるか?なんですが、同じ検索結果の下の方にあるWWDC 2014のビデオ

 

 What's New in SceneKit

 

あたりを軽く見て、Phisycsで何ができるのか把握しておいて、検索結果のGuidesタブ側にある検索結果確認ですな。

 

https://developer.apple.com/search/?q=physics&type=Guides

 

 なんだけど、ヒットするのほとんどSpriteKitの方で、上のビデオで紹介されてるSceneKit側のSCNPhysicsBodyが出ねーんすよ。「SceneKit physics」で検索してもあんま変わらない。

注意)SpriteKitはSceneKitの2D版です。SceneKitより先にこっちが用意された。2DならUIViewとかUIImageViewとかあるじゃんと思うかもしれないけど、UIKit系はボタンやナビゲーションバーや画面遷移といったGUIパーツの構築しやすさを優先してて、ゲームといったグラフィック性能バリバリ使います系にはスピード面でやや劣るんですな。そのため、ゲーム作る人は、UIKitより操作がめんどくさい(でも高速な)OpenGLなんかを使わないとダメだったんですが、その点を改善すべくUIKit風に使い勝手がよくて、OpenGL並みに描画性能が高いKitとしてSpriteKitが登場しますた


 仕方ないんで、とりあえず実際に、上のビデオの18分12秒あたりで紹介されてるように、SCNNodeのphysicsBodyプロパティにSCNPhysicsBodyのdynamicBodyで作ったオブジェクトを割り当ててみようかと。

SCNPhysicsBody

 ビデオによると、SCNNodeは、こいつを割り当てられると物理学に従った挙動をするわけっす。

 

これね

 

 これを実際に試す。

 ベースにするのはXcodeのARテンプレね。

 

 

 

 こいつは「こんにちはAR」、「ARKitだ!」でやった「Single View App」テンプレからのARセットアップまでをテンプレ側で終わらせてくれてるすぐれもの。

 おまけに「見た目が大事」で紹介したSceneファイルからのSCNScene構築までやっちゃってるので、Runさせるだけで、そのまんま空間に宇宙船が浮いたりもするわけですよ。

注意)ちなみにSCNScene構築のために読み込んでるファイルは、COLLADAじゃなくSceneKitのネイティブファイルであるSceneファイル(拡張子.scn)ね

 

 プロジェクト作成手順自体は「Argmented Reality App」を選ぶ以外は「Single View App」と同じっす。

 

てなわけだ

 

 で、こいつはSCNSceneのrootNodeに"ship"て名前のSCNNodeが追加されてるんで、childNode(withName:,recursively:)で取り出せます。

 ここらへんの情報は、提供されてるSceneファイルをScene Editorで見ればわかります。

 

 

 で、取り出したSCNNodeオブジェクト"ship"の、physicsBodyプロパティに

 

 SCNPhysicsBody.dynamic()

 

で作ったオブジェクトを割り当ててみる。

 

ViewController.swift

class ViewController: UIViewController, ARSCNViewDelegate {
    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        ・・・
        // Create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        phygicsLab(scene:scene)
        ・・・
    }
    ・・・
    func phygicsLab(scene:SCNScene) {
        if let ship = scene.rootNode.childNode(
            withName: "ship", recursively: true) {
            ship.physicsBody = SCNPhysicsBody.dynamic()
        }
    }
}

 ビデオではObjective-C言語で

 

  [SCNPhysicsBody dynamicBody];

 

と説明されてるんだけど、swift言語なんで、そのまま記述できんわけなんやね。なので

  [SCNPhysicsBody dynamicBody];

  SCNPhysicsBody.dynamic()

となる。

 ここらへん、Objective-Cとswiftの対応はXcodeのHelp>Developer Documentationメニューで出てくる画面で調べられます。

 

 で、早速Run。

 

 

 

 なんか宇宙船落ちたし(笑。

 一瞬すぎてわからんけど、目の前通り過ぎたような…

 SCNPhysicsBody.dynamic()を割り当てたんで、重力が効いてしまって自由落下しちゃうんですな。

 底がないんで、地球の中心に向かって落下してる模様。

 チューわけで底を用意します。

 あと、床に跳ね返る様子を見やすいように、ship.position.zに-3を指定して、宇宙船を3m後ろに移動させとく。でもって、床の高さも、position.yに-1を設定して1m下にしてます。positionわからん人は「仮想体を置いてみる」ね。

 

ViewController.swift

    func phygicsLab(scene:SCNScene) {
        if let ship = scene.rootNode.childNode(
            withName: "ship", recursively: true) {
            ship.physicsBody = SCNPhysicsBody.dynamic()
            ship.position.z = -3	// 3m後ろに移動させとく
        }
        //  床を追加 静的な物理物体として設定
        let floorNode = SCNNode(geometry: SCNFloor())
        floorNode.physicsBody = SCNPhysicsBody.static()
        floorNode.position.y = -1
        scene.rootNode.addChildNode(floorNode)
    }

 SCNFloorてのは組み込み形状で無限xz平面です。Appleのサンプル色々見てて見つけますた。

 

 で、こいつのphysicsBodyプロパティには、ビデオで紹介されてたように、重力や外から加えられた物理的な力に影響されない静的な物理体(PhysicsBodyを直訳してみた)となるSCNPhysicsBodyを割り付ける。静的な物理体を作る場合は

 

 SCNPhysicsBody.static()

 

を使う。

 これで今回作る床は、空間に固定されたSCNPhysicsBody付きSCNNodeになるわけです。

 空間に固定って、physicsBodyプロパティにSCNPhysicsBodyを割り付けられてないSCNNodeと一緒じゃんと思うかもしれないけど、SCNPhysicsBody.static()を割り当てた場合、SCNPhysicsBody同士が衝突した場合、お互いに弾きあうという性質が利用できるんですよ。

 SCNPhysicsBodyが設定されてないSCNNodeとは、衝突してもそのまますり抜けちゃうんで、宇宙船の落下を止めるには固定されたSCNPhysicsBodyを設定したSCNNodeが必要なんですな。

 これで落下した宇宙船は、固定された床にぶつかって止まる。

 

 Runさせると、無事床に着地しました。

 

ま、ぶっちゃけ、突き刺さったわけだが

 

 おもろいやんけ〜、ということで、画面タップしたら宇宙船に力を加えてみることにしました。

 

ViewController.swift

    func phygicsLab(scene:SCNScene) {
        ・・・
        scene.rootNode.addChildNode(floorNode)
        // 画面タップを見張る
        self.view.addGestureRecognizer(UITapGestureRecognizer(
            target: self, action: #selector(didTap)))
    }
    
    /// 画面タップされたら、宇宙船に力加える
    @objc func didTap() {
        if let ship = sceneView.scene.rootNode.childNode(
            withName: "ship", recursively: true) {
            ship.physicsBody?.applyForce(
            	SCNVector3(x:0, y:8, z:0), asImpulse: true)
        }
    }

 Runすると、画面タップするたびに宇宙船が飛び跳ねるようになります。

 

 

サンプル:

 http://tetera.jp/xcc/book-sample/ARPhysics.zip

 

 空中で浮いてる時にタップすれば、そこからまたジャンプだ。

 無限の宇宙へ!

 

 ここで使ってるapplyForceってメッセージは、SCNPhysicsBodyオブジェクトに対して力を加えるって指定です。Developer DocumentationのSCNPhysicsBodyの説明だと、最初の引数で力の大きさと向き、asImpulse:引数をtrueにすることで、一瞬の衝撃(インパルス:Impulse)ってことになります。

 衝撃指定の場合の第1引数のSCNVector3は力積値を意味するらしい。

 力積て何?

 という話は長くなるんで次回。

 

 

iPhoneアプリ開発:AR 目次

 

 Xcodeではマテリアルを同じような値で指定しても、Blenderの時ほど金属ぽくない。

 

なんか、イマイチ

 

 なぜなのか?

 それはXcodeの方には金属に映り込む風景が存在しないから。

 テカテカする金属の表面を見るてっのは、金属の表面そのものを見てるわけじゃなく、金属の表面に映り込む周りの風景を見てるってことに他ならない。

 で、見る角度によって映り込む風景がチラチラと変わる。明るい風景を反射したら明るくなるし、暗い風景を反射したら暗くなる、この陰陽の変化を脳がとらえて金属っぽいって感じるわけです。

 

 

 なんだけど、Xcodeではその映り込むべき風景が何もない暗黒なんですよ。

 真っ暗闇の中にポツンと光が灯ってる状態。なので金属表面が陰陽の変化の少ないのっぺりした描写になる。で、なんか金属っぽくないなと…

 え、でも、それはBlenderでも同じなんじゃ…と思った人、残念、Blenderでは事前に風景を用意済みなんですな。

 Blenderの3Dビューのボトムバーにあるシェーディングってメニューをクリックするとわかります。

 

実は設定済みというね

 

 ちなみに、円で表示されてる風景をクリックしたら別の風景も選べます。

 

 

 この画像の円形での表示は、球体の内側に貼り付けられてる画像を見てる状態を表現してまして、実際は視線の後ろ側にも画像が広がってます。

 

 

 なので、視線をどこに向けようが、360度全周に画像が存在するってわけです。

 

 

 そのため、割り当てる画像は、球体の裏側360度全周に貼り付けられることを前提にしたものが必要で、縦横比が1:2の横長画像のメルカトル図法っぽい画像が要求されます。Blenderではそうでない画像を指定してもいいんだけど、その場合、360度全周に貼り付けた時にどこかしら破綻する。

 注意)メルカトル図法なのかどうかは知らんけど

 

 具体的に言えばリコーのシータで撮った画像ね。

 

こんな感じね。他人が写ってるんでぼかしたけど

 

 

 

 もしくはAppStoreにあるGoogle ストリートビューみたいなアプリで作成した画像ね。パノラマ撮影の全周版って感じで撮るのめんどくさいけど、iPhone使って無料で上記の全天球用の写真が撮れます。

 

Google ストリートビュー

 

 で、Xcodeでは事前に設定はしてないものの、自分で設定することは可能っす。

 上の写真をダウンロードしてSecneKit Catalogフォルダに入れちゃってください。サンプルだとSceneKit Asset Catalog.scnassetsってフォルダね。

 わからん人は「見た目が大事」を読み返すべし。

 

 

 フォルダに置くと、勝手にXcodeのナビゲーションエリアにファイル名が表示されます。

 そこまでできたら、今度はナビゲーションエリアでCOLLADAファイル選んで、右端のShow the scene inspectorタブを選んで、Environmentメニュークリックして、フォルダに入れた画像ファイルを選択する。

 

 

 これで金属っぽくなる。

 

 

 グレート。

 で、ここまで説明しておいて、またまた残念なお話なんだけど、そもそもCOLLADAファイルをXcode上で、前回やったようにマテリアルのShadingをPhygically basedに変更してもARKit(SceneKit)が読み取ってくれません。

 なんか、もおおおおお、ね、色だけしか読み取ってくれない。

 もう、素直にCOLLADAファイルをSCNファイルに変換してから修正するのがいいみたいっす。ま、そこらへんは「お気楽極楽Blender[4] ダンスダンスダンス後編」で説明してるので各自でやってください。

 その場合、例えばyumbo.daeをyumbo copy.scnに変換した場合はExcavator.swiftのinit()でやってる

 

Excavator.swift

class Excavator {
   ・・・
       init() {
     ・・・
        guard let yumboURL = Bundle.main.url(
          forResource: "SceneKit Asset Catalog.scnassets/yumbo", 
           withExtension: "dae") else {
            return
        }
     ・・・

てのを

class Excavator {
   ・・・
       init() {
     ・・・
        guard let yumboURL = Bundle.main.url(
          forResource: "SceneKit Asset Catalog.scnassets/yumbo copy", 
           withExtension: "scn") else {
            return
        }
     ・・・

に変えるように。当たり前。

 

 COLLADAファイルそのまま使いたい人はプログラムで変更しましょう。

 

サンプル:

 http://tetera.jp/xcc/book-sample/yumbo-wktk.zip

 

Excavator.swift

class Excavator {
   ・・・
    init() {
     ・・・
        shoulder.firstMaterial?.diffuse.contents = UIColor.yellow
        guard let bucketNode = boomNode.childNode(
          withName: Excavator.bucketID, recursively: true) else {
            return
        }
        bucketNode.changeToPhysicallyBased(name:"red")
        boomNode.changeToPhysicallyBased(name:"body")
        root = baseNode
    }
  ・・・
extension SCNNode {
    func changeToPhysicallyBased(name:String) {
        let i = self.geometry?.materials.firstIndex(
          where: { (m:SCNMaterial) -> Bool in
            return m.name == name
        })
        if let i = i, let m = self.geometry?.materials[i] {
            m.lightingModel = .physicallyBased
            m.metalness.contents = NSNumber(value: 1.0)
            m.roughness.contents = NSNumber(value: 0.0)
        }
    }
}

 前回、boomNode以外はオブジェクトとして取り出さなくてもいいようにしたんだけど、結局必要になってしまった…

 bucketNodeはboomNodeの子SCNNodeになってるんで、boomNodeからchildNodeで探し出してる。で、そのbucketNodeとboomNodeのgeometryのmaterialsに設定されてる"red"と"body"って名前のマテリアルを、物理ベースの設定に変更してます。前回Scene Editorで変更したとこね。

 変更にはSCNNodeクラスにextensionでchangeToPhysicallyBased(name:)てメソッドを追加して利用するようにしてます。extensionがわからん人は「リファクタリング」ね。

 geometryのmaterialsから、指定の名前のSCNMaterialを探し出すには、Array構造体が持つfirstIndexメソッドを利用してる。

firstIndex(where:)

 こいつはwhere:引数の後のクロージャに、materials配列内の各SCNMaterial要素が1つずつ

   (引数名:SCNMaterial)-> Bool

という形で渡され、探してるSCNMaterialならtrue、そうでないならfalseを返すという決まりになってるんで、引数であるSCNMaterialに適当な引数名(サンプルではmにした)与えてnameプロパティとnameの比較結果を返すようにしてます。

      {
   (m:SCNMaterial)-> Bool in
            return m.name == name
      }

 配列がわからん人は「色々と脱線」、クロージャがわからん人は「タップしてドン」ね。ただクロージャに関しては任意の処理を{・・・}で書けるとしか説明してないんで、ここでもうちょっと補足すると、必要なら、クロージャ呼び出し側がクロージャに引数を渡せるようにもなっていて、戻り値をもらえるようになっている。その場合{・・・}の初頭に

      { (引数名:引数の型,...) -> 戻り値の型 in

      }

と書くことで、呼び出し側からの引数を受け取り

     return 値

で、呼び出し側に戻り値を返せるようになる。

 それが、上のサンプルでやってること。

 ここら辺はfirstIndexメソッド側が決めることで、使う側は、それに従ったクロージャを用意しなければいけないわけっすね。

 クイックヘルプで調べれば、どんな風にクロージャが呼び出されるかわかるようになってます。

 クイックヘルプがわからん人は「または私は如何にして心配するのを止めて…」ね。

 

 

 なのでfirstIndexのwhere:で渡してるクロージャ

     { (m:SCNMaterial) -> Bool in return m.name == name }

てのは、materials側から、自分の要素SCNMaterialを1つ1つ取り出して、こいつが探してるSCNMaterialですかって聞かれることに対する処理になるわけですよ。

 その処理がm.name == nameならtrueを返すって処理なわけで、これでnameに一致するSCNMaterialをmaterials側から取り出せることになる。

 firstIndexて名前からわかると思うけど、配列内に該当する要素が複数ある場合、最初に見つかった要素のインディックスになります。

 で、見つかったSCNMaterialのlightingModelプロパティに物理ベースのパラメータを指定するよって指示して、関係するmetalness、roughnessプロパティを設定してる。

lightingModel

 「光あれ」で説明したシェーディング時の演算に、物理ベースを指定するには、このプロパティにSCNMaterial.LightingModel.physicallyBasedを指定してやればいいわけです。省略形が.physicallyBasedね。

 他にどんなのがあるか興味ある人はクイックヘルプで調べましょう。

 で、物理ベースで使われるSCNMaterialのプロパティはmetalness、roughnessで、こいつは0.0〜1.0の数値を指定することになってる。

 指定するには、metalness、roughnessのcontentsプロパティにNSNumberで設定します。

NSNumber

 数値を表現するクラスです。contentsプロパティは何らかのオブジェクトでないと指定できないので、float型の値を直接は設定できないんすよ。で、その代用としてNSNumberオブジェクトを使います。NSNumberのイニシャライザのvalue: 引数の後ろに設定したい値を書く。

   m.metalness.contents = NSNumber(value: 1.0)

 これでマテリアル側の設定は完了。

 

 で、.physicallyBasedを指定した場合、重要なのが映り込む風景の指定。

 COLLADAじゃなくSCNファイル使う場合でも、読み込んだSCNSceneをそのままARSCNViewのsceneとして使わないなら、映り込む周辺の画像は自前で設定する必要があります。サンプルではブーム、アーム、バケットの3形状を読み込んで使ってるだけなんでね。

 何言ってるのか、わからん人は「仮想体を置いてみる」、「見た目が大事」を読みなおしましょう。

 

 とにかく、プログラムで風景画像を指定する場合はSCNSceneのlightingEnvironmentプロパティにUIImageとして設定するのが基本っす。

lightingEnvironment

 こいつにはSCNMaterialPropertyクラスのオブジェクトが設定されてて、この中のcontentsプロパティにさっきの風景写真をUIImageとして設定すれば、映り込む風景の指定は完了。

 

ViewController.swift

class ViewController: UIViewController {
    ・・・
    override func viewDidLoad() {
      ・・・
        yumbo.root.position = SCNVector3(x:0, y:-1, z:-3)
        scene.rootNode.addChildNode(yumbo.root)
        scene.lightingEnvironment.contents = UIImage(
          named: "SceneKit Asset Catalog.scnassets/environment.JPG")
    }

 で、まあ、これで十分テカテカなんだけど、クイックヘルプの.physicallyBasedの説明では.physicallyBased使うなら、SCNCameraのwantsHDRプロパティをtrueにしろとか書かれてるんで、やってみました。

wantHDR

 HDRてのはHigh Dynamic Range(ハイダイナミックレンジ:豊かな階調)のイニシャルです。例えば黒にも様々な黒があるわけで、漆黒から淡い黒まで、きめ細やかに表現する場合に指定します。

 wantsHDRプロパティをtrueにすることで、HDR希望ってことになるのかな。このプロパティを持ってるオブジェクトが

SCNCamera

 こいつは仮想体撮影用の仮想カメラっす。

 でもって画面描画に使用されるSCNCameraは、ARSCNView(その派生元であるSCNView)のpointOfViewプロパティのcameraプロパティからゲットできます。

pointOfView

 このプロパティには視点となるSCNNodeが指定されるっぽい。

 例えばバケットSCNNodeのcameraプロパティにSCNCameraを設定しておいて、このpointOfViewプロパティにバケットSCNNodeを設定すると、バケットから見た風景が表示されるんじゃないかと…

 ゲームで言うところの本人視点シューティングゲームぽく。

 ま、それは置いといて、とにかくwantsHDRプロパティを設定。

 

ViewController.swift

class ViewController: UIViewController {
    ・・・
    override func viewDidLoad() {
      ・・・
        scene.lightingEnvironment.contents = UIImage(
          named: "SceneKit Asset Catalog.scnassets/environment.JPG")
        if let camera = sceneView.pointOfView?.camera {
            camera.wantsHDR = true
        }
    }

 うーん、変わらんような…

 よくわかりません。とりあえずサンプルはRunするとこんな感じになる。

 

 

 ちなみに、このダイアモンドカット的なエッジの効いたバケットもいいけど、曲線部は滑らかにしたいよねえな人は、Blenderで、bucketオブジェクト選んだ状態で、3Dビューのボトムバーからオブジェクト>スムーズ面メニューを選びましょう。

注意)もちろんXcode側で設定する方法もあるが、それはいずれ

 

 

スムーズ面

 スムーズ面指定するとこうなる。


 

 元に戻したい場合は、オブジェクト>フラット面メニューを選びましょう。

 これって、シェーディング時に法線をどう扱うかって話なんだけど、ARKitでもgeometryオブジェクト単位では対応してくれるみたいっす。

 

 

法線によって、周辺の風景のどの部分が反射して見えるかを計算してる

 

 法線、なんすかそれな人は「光あれ」を読み返しましょう。

 

 Blenderオンリーだと、もうちょっと小技が指定できて、プロパティビューのオブジェクトタブに、面と面が指定した角度以下の場合はスムーズ面にしないって指定もあったりする。

 これだと側面は平坦になる。

 

 

 ただしARKitは認識してくれませんでした。

注意)もっと細かいので、編集モードでスムース面化させたい面だけ選んでおいて、面>スムーズ面を選ぶってのもあるんだけど、もちろんARKitは未対応

 

 で、法線もそうだけど、色の分布や粗さ具合の分布を平面単位じゃなく、平面に対応する2Dの分布図として設定するには、マテリアルタブの下にあるテクスチャタブを使います。

 

 

 MMDでは使ってるし、ARKitでも表示されたんで、ある程度対応できてるぽいけど、どこまで対応かは不明。

 ちなみにMMDのテクスチャ設定はマテリアルタブの下の方にあったりします。

 

 テクスチャタブの下の物理演算タブなんかも、フォースフィールドやコリジョン、剛体なんかあって面白いんだけど、COLLADAファイルでどこまで引き継げるか、ARKitでどこまで対応なのか不明。というか対応してないっぽい。

 

 

 というわけで、Blenderの話は一旦終了。

 いよいよ、Xcodeで「ダンスダンスダンス後編」で軽く触れた、リジッドボディや物理計算に突入します。

 突入せよ!あさま山荘。

 

iPhoneアプリ開発:AR 目次

 
 てなわけでバケット(bucket)は省略。
 これまでに紹介したやり方で作れるからね。
 
これね
 
 ブーム、アーム、バケットはそれぞれローカル空間で、次のような原点にしておくと、ARKit側では読み込んで親子関係結ぶだけで済みます。
 ピボット調整(光あれでやったpivotプロパティね)しなくていいわけです。
 
 
 ちなみに各オブジェクトのローカル空間での素の状態(グローバル側でトランスフォームされてない)を確認するには、オブジェクトモードでオブジェクト選んでおいて、3Dビューのボトムバーからオブジェクト>クリア>位置オブジェクト>クリア>回転オブジェクト>クリア>拡大縮小を選べばいいんだけど、これって一時的にクリアするとかできないもんなのか?
 めんどくさいんだが…
 マニュアルに何か書いてるかもしれんが未確認。
 ちゃんとクリアされてるかはnキー打ちでサイドバー出してトランスフォームグループの位置や回転が0.0に、拡大縮小が1.0になってるのを確認すればよくってよ。
 プロパティビューのオブジェクトタブのトランスフォームグループ側でもOK。どっちも連動してます。
 上記のクリアメニュー使わずに、ここで直接値を入れるってのもありです。
 
 
 確認して思ってた位置とずれてたら編集モードで調整。オブジェクトモードのまま移動させても、ローカル空間での位置調整にはならんでな。
 調整できたらオブジェクトモードに戻る。
 オブジェクトモードでは見やすい位置に配置しても問題ナッシング。さっき言ったようにローカル空間での形状の位置には影響しない。
 実際「見た目が大事」ではグローバル側の位置をずらしてるのを読み込ませてる。別に重なったままでもいいけどね。
 
 
 COLLADAでの書き出しは「親子関係を確認」を参考にしましょう。今回はモディファイアーを適用にチェックだ!
 何の指定かはもうわかるよね。
 
 「親子関係を確認」でやったようにブーム、アーム、バケット、3つのオブジェクトを別々に読み込んで、その後親子関係結ぶのもいいんですが、Blender側で親子関係結んでおくってのも有りです。
 
 
 この場合、AR側ではブームだけを探して、土台と親子関係結んでおしまい。アームやバケットの連結作業は不要になる。
 
Excavator.swift
class Excavator {

    init() {
        ・・・
        //  ブーム
        guard let boomNode = yumboScene.rootNode.childNode(・・・
            return
        }
        shoulderNode.addChildNode(boomNode)
        //  アーム
        guard let armNode = yumboScene.rootNode.childNode(・・・
            return
        }
        boomNode.addChildNode(armNode)
        //  バケット
        guard let bucketNode = yumboScene.rootNode.childNode(・・・
            return
        }
        armNode.addChildNode(bucketNode)
        //  土台はpositionの示す位置が底辺になるようにする
        baseNode.pivot = SCNMatrix4MakeTranslation(・・・
        //  肩である球体は土台の最上部が、球の中心位置になるように配置
        shoulderNode.position = SCNVector3(・・・
        //  ブームはpositionの示す位置が底辺になるようにし・・・
        boomNode.position = SCNVector3(・・・
        //  アームはpositionの示す位置が底辺になるようにし・・・
        armNode.position = SCNVector3(・・・
        //  バケットはアームの最上部に配置
        bucketNode.position = SCNVector3(・・・
        base.firstMaterial?.diffuse.contents = UIColor.red
        shoulder.firstMaterial?.diffuse.contents = UIColor.yellow
        root = baseNode
    }
 
サンプル:
 
Blenderファイル:
 ただ、これを試してみて気づいたんだけど、XcodeではCOLLADAファイルのオブジェクトの名前にスペースが入ってると、そこで名前が確定するみたいっす。
 
 arm line 1
 arm line 2
 arm line 3
 
は、どれもarmの後ろにスペースがあるんで、全部armになっちゃう。
 なんで、あっちゃこっちゃの階層にarmってオブジェクトができて、検索に支障をきたすんですな。boomも同じ。
 
 
 最上位のワールド空間に3つのオブジェクトを配置したい場合は、最初に見つかるのは最上位のboom、arm、bucketなんで問題なかったんだけど、階層構造にしておくと、boomからのarm、bucketの検索になるんで具合悪い。
 何言ってるかわからん人は「ムーブムーブムーブ」のchildNodeメッセージの話を読みましょう。
 なので、さっきのサンプルのyumbo.blendは「arm line 1」てのを「arm-line-1」て風にスペースを - (ハイフン)に置き換えてます。
 実際使ってみると色々あるわな。
 形状についてはこんなところで。
 
 最後にマテリアルについて。
 ブームやアーム作って気づいたと思うけど、Blenderは新規作成のオブジェクトの色は白色にしてるんですよ。これを油圧ショベルっぽく、各パーツを黄色にするって話っす。

マテリアル

 「光あれ」で説明したように3Dの場合、単純に色を指定するって話にはなりません。金属っぽいとかザラついてるとか、いくつかのパラメータで表面の質感を指定することになるんですな。
 これがマテリアルの指定。
 このマテリアルの指定、Blender自体はオブジェクトといわず、面の1つ1つに個別のマテリアルを指定できるんだけど、ARKitがどこまで対応してるかは不明。
 オブジェクト単位では対応してるみたいなので、今回はオブジェクトごとの指定方法を説明します。
 なのでまずはオブジェクトを選択。
 bucketあたりを選択しちゃってください。
 
 
 選択したら、プロパティビューのマテリアルタブを選択。表示される画面がマテリアルの設定画面っす。

マテリアルタブ

 bucketにはすでにbodyてマテリアルを作って設定してるんで、こんな感じの画面になる。
 
 
 
 見ての通りSCNMaterialでも扱えそうな名前が並んどります。SCNMaterialのプロパティ名と関連させるとこんな感じかな。
 
 ベースカラー:diffuse
 メタリック:metalness
 粗さ:roughness
 
 スペキュラーは、どうなるか不明。
 ちなみに「ノードを使う」は今のところ使わない方向で。
 めちゃめちゃ、きめ細やかなパラメータ指定ができるんだけど、ARKit側で対応してないっす。対応する場合、自分でカスタムシェーダーを用意する必要がある。
 カスタムシェーダーについてはいずれ。
 

 
 とりま、ベースカラーをいじりゃ色が変わります。
 金属っぽくしたけりゃメタリックを1.0よりにすればいいし、ツルツルの鏡面仕上げにしたけりゃ粗さを0.0寄りにすればいい。
 で、その結果がどうなるかがプレビューで確認できるんだけど、いまいち分かりずらいっす。
 
あんま効果がわかりずらいんだ、これが
 
 こんな時は、もう、直接3Dビューで確認しちゃっていんじゃないかと思われ。
 3DビューのボトムバーのシェーディングをLookDevに変えましょう。
 注意)見えない場合、ボトムバー上にマウスを合わせて右にスクロール
 
 
 でマテリアルbodyのパラメータ、メタリックを1.0、粗さを0.0にしてから、3Dビューで視点をグリグリ動かしてみる。
 

 

 

 
 めちゃめちゃ、金属っぽいっしょ。
 色々パラメータ変えて変化の具合をみてください。
 ちなみにマテリアルbodyは、ブームやアームにも設定されてるんで、一緒に変化してます。
 で、例えばbucketだけのマテリアルを用意したい場合は、新規にマテリアルを作成する必要がある。
 その場合は、マテリアル名の横にある「×」マークをクリック。
 これでbucketだけ、割り当てられたマテリアルbodyが解除され、新規ボタンが表示されることになる。
 
 
 で、新規ボタンをクリックすれば、新しくマテリアルが作成されてbucketに割り当てられます。
 
 
 redとかつけてみる。 
 
 
 で、こうなる。
 
 
 で、まあ、ここまで書いててなんだけど、残念な知らせがあります。
 
 バージョン2.90βのBlenderで設定したマテリアル、ARKitで理解できないみたいっす。
 
 頭痛い。
 注意)「ダンスダンスダンス後編」で使ったscntoolでSCNファイルにコンバートしてもダメでした。しかも冗長指定で変換処理を細かくレポートさせても特に何も言ってこない
 
 
 
 まあ、ベータ版だしね。
 今はXcode側でマテリアル設定するしかないです。
 ちなみに、今回の元凶はマテリアルのパラメータTransparentに設定されてる色っぽい。
 Xcodeのナビゲーションエリアでyumbo.daeを選んで、インスペクターエリアのMaterial inspectorタブ選んで、Transparentを変更します。
 
 不透明の白に変更。
 
 
 あとはShadingをPhygically based(こっちも読み込み時に、判定できてないみたい)にして、MetalnessやRoughnessを調整しておしまい。
 
 
 ふう。
 ただ、これ指定しても、Blenderの時みたいにピカピカにはなりません。
 何でかというと、金属面が反射するべき周りの風景を何も指定してないから。そこらへんは次回。