Pythonista3 でARKit 拡張現実体験をするよーーー！！！

この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の19日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone（Pythonista3）で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

```sysInfo.log
--- SYSTEM INFORMATION ---
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.1.1, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
```

他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。

ちなみに、`model iPhone12,1` は、iPhone11 です。

## この記事でわかること

- ARKit をPythonista3 で起動する
- 前面カメラで世界を映す
- AR 世界にオブジェクトを出す

## Pythonista3 の拡張現実の世界へようこそ

拡張現実（AR）の開発ができるARKit も、`objc_util` から呼び出しが可能です。

AR とは、ARKit とは。の部分は別の解説に譲るとして、この記事ではPythonista3 でAR が使える状態になるまでを紹介します。

[ARKit - 日本語ドキュメント - Apple Developer](https://developer.apple.com/jp/documentation/arkit/)

### `RealityKit` は呼び出せないかも

iOS 関係でAR を調べると、ARKit 以外に`RealityKit` もありますが、Pythonista3 でRealityKit は（私の観測上だと）呼び出せません。

（私が察する理由ですが）Swift でしか開発ができないので、`objc_util` で呼び出せないのだと思われます。

自分が実装してみたい内容の背景が、何で構成されているのかの確認は必要ですね。

[RealityKitの概要 - 拡張現実 - Apple Developer](https://developer.apple.com/jp/augmented-reality/realitykit/)

### ARKit とSceneKit（または、SpriteKit） の連携

前回までのSceneKit の技術をたくさん使います。

ARKit で取得した現実情報とSceneKit の`SCNScene` を結びつけ画面上に登場させるイメージです。

「これはARKit での処理、こっちはSceneKit の処理」と課題の分離ができると、確認用のミニマムな実装も行いやすいと思います。

```イメージ
ARSCNView → SCNView からAR のView に変更
  |
SCNScene
  |
rootNode
↑ ↑ ↑ ↑ addChildNode(Node)
さまざまなNode たち

```

## AR の世界へダイブ

ARKit のミニマムな内容と、SceneKit 生成したBox を呼び出します。

リアカメラから現実世界を取得し、SceneKit のBox はその場に止まっている状態です。

ちなみに`load_framework('SceneKit')` の呼び出しは不要です。

`load_framework('ARKit')` とARKit をロードすることで、SceneKit も呼び出されます。

```py
from objc_util import load_framework, ObjCClass
from objc_util import UIColor
import ui

import pdbg

load_framework('ARKit')

ARSCNView = ObjCClass('ARSCNView')
ARWorldTrackingConfiguration = ObjCClass('ARWorldTrackingConfiguration')

SCNScene = ObjCClass('SCNScene')
SCNNode = ObjCClass('SCNNode')

SCNAction = ObjCClass('SCNAction')

SCNBox = ObjCClass('SCNBox')


class GameScene:
  def __init__(self):
    self.scene: SCNScene
    self.setUpScene()

  def setUpScene(self):
    scene = SCNScene.scene()
    scene_rootNode_addChildNode_ = scene.rootNode().addChildNode_

    box = SCNBox.boxWithWidth_height_length_chamferRadius_(0.5, 0.5, 0.5, 0.08)
    box.firstMaterial().diffuse().contents = UIColor.blueColor()

    geometryNode = SCNNode.nodeWithGeometry_(box)
    geometryNode.position = (0, -1.0, -1.0)

    geometryNode.runAction_(
      SCNAction.repeatActionForever_(
        SCNAction.rotateByX_y_z_duration_(0.0, 0.2, 0.1, 0.3)))

    scene_rootNode_addChildNode_(geometryNode)

    self.scene = scene


class ViewController:
  def __init__(self):
    self.sceneView: ARSCNView
    self.scene: GameScene
    self.viewDidLoad()
    self.viewWillAppear()

  def viewDidLoad(self):
    scene = GameScene()

    _frame = ((0, 0), (100, 100))
    sceneView = ARSCNView.alloc().initWithFrame_(_frame)
    sceneView.autoresizingMask = (1 << 1) | (1 << 4)
    sceneView.autoenablesDefaultLighting = True
    sceneView.showsStatistics = True
    ''' debugOptions
    OptionNone = 0
    ShowPhysicsShapes = (1 << 0)
    ShowBoundingBoxes = (1 << 1)
    ShowLightInfluences = (1 << 2)
    ShowLightExtents = (1 << 3)
    ShowPhysicsFields = (1 << 4)
    ShowWireframe = (1 << 5)
    RenderAsWireframe = (1 << 6)
    ShowSkeletons = (1 << 7)
    ShowCreases = (1 << 8)
    ShowConstraints = (1 << 9)
    ShowCameras = (1 << 10)
    ARSCNDebugOptionShowFeaturePoints = (1 << 30)
    ARSCNDebugOptionShowWorldOrigin = (1 << 32)
    '''

    _debugOptions = (1 << 1) | (1 << 30) | (1 << 32)
    sceneView.debugOptions = _debugOptions

    sceneView.scene = scene.scene
    sceneView.autorelease()

    self.scene = scene
    self.sceneView = sceneView

  def viewWillAppear(self):
    self.resetTracking()

  def viewWillDisappear(self):
    self.sceneView.session().pause()

  def resetTracking(self):
    configuration = ARWorldTrackingConfiguration.new()
    self.sceneView.session().runWithConfiguration_(configuration)


class View(ui.View):
  def __init__(self, *args, **kwargs):
    ui.View.__init__(self, *args, **kwargs)
    self.bg_color = 'maroon'
    self.vc = ViewController()
    self.objc_instance.addSubview_(self.vc.sceneView)

  def will_close(self):
    self.vc.viewWillDisappear()


if __name__ == '__main__':
  view = View()
  view.present(style='fullscreen', orientations=['portrait'])

```

### SceneKit からARKit へ書き換え

前回のSceneKit では、`objc_util` とPythonista3 の`ui.View` の連携として`View` class 内で完結をさせていました。

今回は`ARSCNView` のView 実装処理記述が多くなるため`ui.View` とは別に`ViewController` class を用意しました。

`objc_util` としての、View 設定に関しては`SCNView` と大きな変化はありません。

サンプルコードに乗っ取り、 UIView のライフサイクルを採用していますが

ViewController のあとに、View 呼んでいるので、なんじゃこりゃって感じなのですが。。。

#### `debugOptions`  のEnumeration Case

`ARSCNView` の`debugOptions` も`SCNView` のEnumeration Case を流用して呼び出せます。

```py
''' debugOptions
OptionNone = 0
ShowPhysicsShapes = (1 << 0)
ShowBoundingBoxes = (1 << 1)
ShowLightInfluences = (1 << 2)
ShowLightExtents = (1 << 3)
ShowPhysicsFields = (1 << 4)
ShowWireframe = (1 << 5)
RenderAsWireframe = (1 << 6)
ShowSkeletons = (1 << 7)
ShowCreases = (1 << 8)
ShowConstraints = (1 << 9)
ShowCameras = (1 << 10)
ARSCNDebugOptionShowFeaturePoints = (1 << 30)
ARSCNDebugOptionShowWorldOrigin = (1 << 32)
'''

_debugOptions = (1 << 1) | (1 << 30) | (1 << 32)
sceneView.debugOptions = _debugOptions
```

- `ARSCNDebugOptionShowFeaturePoints`
  - `1 << 30`
  - トラッキングされるポイントを可視化
- `ARSCNDebugOptionShowWorldOrigin`
  - `(1 << 32)`
  - AR の座標軸を可視化

の2点が`ARSCNView` にて追加されています。

- [ARSCNDebugOptionShowFeaturePoints | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arscndebugoptionshowfeaturepoints?language=objc)

https://developer.apple.com/documentation/arkit/arscndebugoptionshowfeaturepoints?language=objc

- [ARSCNDebugOptionShowWorldOrigin | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arscndebugoptionshowworldorigin?language=objc)

https://developer.apple.com/documentation/arkit/arscndebugoptionshowworldorigin?language=objc

`(1 << 30)`, `(1 << 32)` の数値は:

```py
def setDebugOptions(arscn):
    # Work In Progress Here, I'm trying to decipher the arkit constants...
    #val = ARSCNDebugOption.ARSCNDebugOptionShowWorldOrigin | ARSCNDebugOption.ARSCNDebugOptionShowFeaturePoints
    val = int("fffffffffc000000", 16) # this value is a combination of ShowWorldOrigin and ShowFeaturePoints flags, but I can't isolate each flags....
    print('Before calling setDebugOptions_(%s) : debugOptions=%s' %(hex(val), hex(arscn.debugOptions())))
    arscn.setDebugOptions_(val)
    print('After calling setDebugOptions_(%s) : debugOptions=%s' % (hex(val),hex(arscn.debugOptions())))
```

ここから無理やり割り出したように記憶しています。。。

[main.py#L61 | Pythonista/main.py at master · Brun0oO/Pythonista](https://github.com/Brun0oO/Pythonista/blob/master/arkit/main.py#L61)

#### AR 機能を実行させる

`SCNView` は、View へ`addSubview_` すれば描画されました:

```py
ui.View.objc_instance.addSubview_(SCNView)
```

`ARSCNView` では「AR やるでー」と宣言しないと、カメラが起動しAR が始まりません。

「AR やるでー」の機能を司っているのは`ARSession` class です。

[ARSession | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arsession?language=objc)

https://developer.apple.com/documentation/arkit/arsession?language=objc

`ObjCClass('ARSession')` と呼び出さなくとも、`ARSCNView` で`ARSCNView.session` として、`ARSession` を呼び出せます。

```py
def resetTracking(self):
  configuration = ARWorldTrackingConfiguration.new()
  self.sceneView.session().runWithConfiguration_(configuration)
```

`ARWorldTrackingConfigurat` で`World`（リアカメラ）となり、フロントカメラを使った選択はここでおこないます。

![img221211_153618](https://user-images.githubusercontent.com/53405097/207570643-2e9df0ef-ee9b-4761-9d24-4335f4c2228c.png)


起動しているかどうかは、ステータスバーの右上に緑の`●` が付きます。

「AR おわったでー」と、宣言させないと、終了してくれないので:

```py
def viewWillDisappear(self):
  self.sceneView.session().pause()


class View(ui.View):
  def __init__(self, *args, **kwargs):
    ui.View.__init__(self, *args, **kwargs)
    self.bg_color = 'maroon'
    self.vc = ViewController()
    self.objc_instance.addSubview_(self.vc.sceneView)

  def will_close(self):
    self.vc.viewWillDisappear()
```

Pythonista3 の`ui.View` 、終了処理時の`ui.View.will_close` より、終了を呼んでいます。

### 世界にSceneKit のジオメトリを呼ぶ

`SCNScene` 内に、SceneKit 実装時と同様の処理で呼び出すことができます。

カメラ（`SCNCamera`）はリアカメラとなるので、呼び出し不要です。

```py
def setUpScene(self):
  scene = SCNScene.scene()
  scene_rootNode_addChildNode_ = scene.rootNode().addChildNode_

  box = SCNBox.boxWithWidth_height_length_chamferRadius_(0.5, 0.5, 0.5, 0.02)
  box.firstMaterial().diffuse().contents = UIColor.blueColor()

  geometryNode = SCNNode.nodeWithGeometry_(box)
  geometryNode.position = (0, -1.0, -1.0)

  geometryNode.runAction_(
    SCNAction.repeatActionForever_(
      SCNAction.rotateByX_y_z_duration_(0.0, 0.2, 0.1, 0.3)))

  scene_rootNode_addChildNode_(geometryNode)

  self.scene = scene

```

`box` のサイズが`SCNBox.boxWithWidth_height_length_chamferRadius_(0.5, 0.5, 0.5, 0.08)` かなり小さいです。

大きすぎると、Material の設定によっては何も描画されないのでお気をつけください。

## 次回は

SceneKit からARKit への流れですと想定より簡単にAR が実装できたのではないかと思います。

`SCNScene` 内の`box` は、呼び出し時の位置から移動せず、カメラが動くと見切れてしまいました。

次回は、現実世界の情報をトラッキングして、良き位置に配置できるようにしていきます。

ここまで、読んでいただきありがとうございました。

## せんでん

### Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

https://t.co/0IOW2hn1VC

### 書籍

iPhone/iPad でプログラミングする最強の本。

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「活用篇」：hirax](https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99)

https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「土台篇」：hirax](https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r)

https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r

### その他

- サンプルコード

[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) でのコードをまとめているリポジトリがあります。

コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー

https://github.com/pome-ta/Pythonista3AdventCalendar2022sampleCode

- Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

https://twitter.com/pome_ta93

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか？って話よ！（クズ）<br><br>Pythonista3 Advent Calendar 2022 <a href="https://t.co/JKUxA525Pt">https://t.co/JKUxA525Pt</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; pome-ta (@pome_ta93) <a href="https://twitter.com/pome_ta93/status/1588570697777152006?ref_src=twsrc%5Etfw">November 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

https://github.com/pome-ta