SCNIKConstraintを使ってロボットの腕を自由自在にアニメーションさせる

SceneKitで3Dモデルをアニメーションさせる方法はいくつかの選択肢がありますが、3Dモデルの腕や足を自由自在にアニメーションさせようとなると、選択肢はSCNIKConstraintか、SCNAnimationになってくると思います。

<アニメーションの方法>
- SCNTransaction　・・・　シーン全体をノードの回転や移動を使って一斉にアニメーションさせる
- SCNAction　・・・　個別のノードについてノードの回転や移動を使ってアニメーションさせる
- SCNIKConstraint　・・・　連結されたボーンを使って操り人形のように動かす
- SCNAnimation　・・・　事前に設定されたアニメーションを実行する

それぞれの違いはこちらの記事が詳しいです。

[SceneKitにおけるアニメーションの実施方法と使い分けについて](https://qiita.com/noby111/items/a2d517178f772c481b65)

SCNAnimationは事前に定義しておけば自由自在に操作できますが、アニメーションの速度や移動量をインタラクティブに操作したいときにはSCNIKConstraintが適しています。

ということで、SCNIKConstraintでロボットの腕を自由自在にアニメーションさせる方法を説明します。

### SCNIKConstraintとは？

SCNIKConstraintのIKは、[Inverse kinematics](https://en.wikipedia.org/wiki/Inverse_kinematics)と呼ばれるものです。

こちらの記事がわかりやすかったので引用します。
[Inverse Kinematicsの実装メモ](https://qiita.com/edo_m18/items/579898d4b2d5ed8ad849)

>　IKとは、人間がとある場所に手を伸ばすときのように手、手首、腕、肩と連動して体を動かすように、連結されたものがとある点に向かうようにする動作を言います。

SCNIKConstraintは、SceneKitのノードにこのIKを適用できるようにするための仕組みです。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/bf8a001b-3ff0-2efe-9723-e78372365027.png">

参考：
[Apple公式: SCNIKConstraint](https://developer.apple.com/documentation/scenekit/scnikconstraint)

### 手順

今回は、ロボットの左腕がドラッグ量にあわせて移動するようなアニメーションを作ります。

**1.3Dモデルを用意する**

3Dモデルにはボーンの設定が入っているものが必要です。
今回は、既存のモデルを使用します。

AppleのARKitのサンプルプロジェクトにロボットの3Dモデル（ファイル名： robot.usdz）が付属しているので、これを使用します。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/4c7360aa-d804-99f8-f5d9-f5d684a5f041.png">

[Capturing Body Motion in 3D](https://developer.apple.com/documentation/arkit/capturing_body_motion_in_3d)

なお、こちらの記事にはモデリングツールで作っている例がありました。

[【SceneKit】SCNIKConstraintの使い方](https://qiita.com/Kirikabu_ueda/items/c6a826b5c4aa25f0aadc)

**2.Xcodeのプロジェクト作成からGameのプロジェクトを作成する**

**3.プロジェクトにusdzのファイルをドラッグ & ドロップする**

usdz形式の3DモデルをSceneKitに読み込む方法はこちらの記事をご覧ください。

[usdz形式の3DモデルをSceneKitに読み込みんでアニメーションさせる](https://qiita.com/TokyoYoshida/items/383414dbc72da51887e1)

**4.画面がドラッグされたら検知するようにする**

viewDidLoadで、UIPanGestureRecognizerを登録します。

```swift:GameViewController.swift
let tapGesture = UIPanGestureRecognizer(target: self, action: #selector(handleDrag(_:)))
scnView.addGestureRecognizer(tapGesture)
```

**5.起点と終点となる２つのジョイントを決定する**

Xcodeでモデルのボーンの情報を確認しながら、起点と終点（実際に動く部分）となる２つのジョイントを決定します。
今回は、左の肩のあたりを起点として、左手を終点として移動させるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/7504b461-096a-b32a-85b4-73c84b6991fd.png)

２点のジョイントは適切に選ばないと、動かないか、おかしな動きになります。

例えば、腰と左手の２点を選んだらサタデーナイトフィーバーのジョン・トラボルタのようになってしまいました。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/6eadbc49-0540-4714-11dc-e90b4103b911.png">

**6.ドラッグされたらロボットの腕が動くようにする**

ドラッグの移動量を取得し、それを元にロボットの腕の移動先を決定します。

```swift:GameViewController.swift
@objc
func handleDrag(_ gestureRecognize: UIPanGestureRecognizer) {
    // アニメーションの起点となる場所を取得する（ここでは左腕の肩）
    guard let root = scene.rootNode.childNode(withName: "left_shoulder_1_joint", recursively: true) else { return }
    // 実際に移動させる場所を取得する（ここでは左腕の手）
    guard let target = scene.rootNode.childNode(withName: "left_hand_joint", recursively: true) else { return }

    // SCNIKConstraintに、起点と移動させる場所を指定する
    let ik:SCNIKConstraint = SCNIKConstraint.inverseKinematicsConstraint(chainRootNode: root)
    target.constraints = [ik]

    // ドラッグの移動量から、ロボットの腕の移動量を決定する
    let move = gestureRecognize.translation(in: view)
    let targetPosition = SCNVector3(target.position.x + Float(move.x * 0.1), target.position.y + Float(move.y * 0.1 * -1), target.position.z)

    // 腕を移動させる
    ik.targetPosition = targetPosition
}
```

### 仕上がり

![20201003qiita用.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/db172255-0c04-9b98-e994-4a6f20f30a3a.gif)

iOSアプリ開発に関する情報を発信しています。
Twitterでも発信していますのでフォローして下さい。
https://twitter.com/jugemjugemjugem