この記事は、Pythonista3 Advent Calendar 2022 の20日目の記事です。
一方的な偏った目線で、Pythonista3 を紹介していきます。
ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。
以下、私の2022年12月時点の環境です。
--- 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 のAR World Tracking 処理
- Tracking 情報から、パネルを張る
-
objc_util
のdelegate
実装
現実世界の情報を掴む
前回は、ARKit の最小の実装方法を紹介しました。
最小の実装では、SceneKit で呼び出したbox
は、その場で回転し続けるのみでした。
今回は、カメラからの現実世界の平面を見つけた際に、平面のパネルを呼び出すようにしてみましょう。
SceneKit 側の処理は通常通りなのですが、objc_util
で呼び出すdelegate
処理が面倒かつ特殊です。
objc_util
のdelegate
を中心に見ていきましょう。
delegate
?
「指定の事柄が発生したら呼び出される」処理です。めちゃくちゃざっくりですが。。。
AVAudioEngine
回でBlock
処理を使いましたが、objc_util
的には似た面倒さで似た処理です(私的には)。
今回ですと「平面情報を取得したら」の条件下で処理が実行されます。
create_objc_class | objc_util — Utilities for bridging Objective-C APIs — Python 3.6.1 documentation
objc_util.create_objc_class
で、新たにobjc_util
オブジェクトのclass を作ることでdelegate
が実装できます。
参照している実装コードで、Delegate 処理が出てきた場合には、Documentation で検索をし必要な情報を探していきます。
Objective-C、Swift のDelegate に関しては、以下の記事から掘っていくと理解が深まるかもしれません。
SwiftにおけるDelegateとは何か、なぜ使うのか - Qiita
methods
methods
に、delegate
で処理されるメソッドを指定します。
このメソッドは、delegate
に存在するメソッド名を定義して、その中で処理を書いていくことになります。
引数には、実在するメソッドの引数の他に_self, _cmd
を先頭に設置する必要があります。
renderer:didAddNode:forAnchor: | Apple Developer Documentation
配列で格納されているように、複数のメソッドを指定できます。
protocols
delegate
は、Protocol として定義されているので、呼び出したいdelegate
を指定します。
今回はARSCNView
のdelegate
なので、ARSCNViewDelegate
です。
ARSCNViewDelegate | Apple Developer Documentation
def create_delegate(self):
メソッド
Pythonista3 の他のdelegate
実装は、メソッドを外に書いて、メソッド内の変数をグローバル化しています。
実装上は、何の問題もありません。ただ私のこだわりとして、class 内で定義をし、スコープをclass 内に収めています。
処理上で、面倒になってしまった場合には素直にglobal
でいいと思います。。。。
実装
前回のコードから、コードを追加して実装していきます。
ViewController
class を定義したのは、この処理からARSCNView
でコネコネすることが多くなるので、コードの見通しの観点からView
class と分けています。
from math import pi
from objc_util import load_framework, ObjCClass, ObjCInstance, create_objc_class
from objc_util import UIColor
import ui
import pdbg
load_framework('ARKit')
ARSCNView = ObjCClass('ARSCNView')
ARWorldTrackingConfiguration = ObjCClass('ARWorldTrackingConfiguration')
SCNScene = ObjCClass('SCNScene')
SCNNode = ObjCClass('SCNNode')
SCNPlane = ObjCClass('SCNPlane')
class GameScene:
def __init__(self):
self.scene: SCNScene
self.setUpScene()
def setUpScene(self):
scene = SCNScene.scene()
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)
_delegate = self.create_delegate()
sceneView.delegate = _delegate
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()
_configuration.planeDetection = (1 << 0)
self.sceneView.session().runWithConfiguration_(_configuration)
def create_delegate(self):
# --- /delegate
def renderer_didAddNode_forAnchor_(_self, _cmd, renderer, _node, _anchor):
node = ObjCInstance(_node)
planeAnchor = ObjCInstance(_anchor)
_width = planeAnchor.planeExtent().width()
_height = planeAnchor.planeExtent().height()
geometry = SCNPlane.planeWithWidth_height_(_width, _height)
geometry.firstMaterial().diffuse().contents = UIColor.color(
red=1.0, green=0.0, blue=0.0, alpha=0.8)
planeNode = SCNNode.nodeWithGeometry_(geometry)
planeNode.eulerAngles = (-pi / 2.0, 0.0, 0.0)
node.addChildNode_(planeNode)
def renderer_didUpdateNode_forAnchor_(_self, _cmd, renderer, node, anchor):
pass
# --- delegate/
_methods = [
renderer_didAddNode_forAnchor_,
renderer_didUpdateNode_forAnchor_,
]
_protocols = ['ARSCNViewDelegate']
renderer_delegate = create_objc_class(
'renderer_delegate', methods=_methods, protocols=_protocols)
return renderer_delegate.new()
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'])
先に、objc_util
側の処理(delegate
)から見ていきます。
delegate
の実装
class ViewController
に、create_delegate
メソッドを生やしています。
メソッド内で、Delegate として呼び出すメソッドを定義しています。
objc_util.create_objc_class
で、class 化し.new()
でインスタンス化したものを返しています。
ViewController.viewDidLoad
上でARSCNView.delegate
へ格納することでDelegate として機能します。
def create_delegate(self):
# objc_util でDelegate を実装するための、メソッド準備
# --- /delegate
# インスタンスメソッド内で、関数を定義する
# 1つめ
def renderer_didAddNode_forAnchor_(_self, _cmd, renderer, _node, _anchor):
node = ObjCInstance(_node) # 引数の変数を、インスタンス化する
planeAnchor = ObjCInstance(_anchor) # tracking で得た情報
_width = planeAnchor.planeExtent().width()
_height = planeAnchor.planeExtent().height()
# SceneKit での生成方法と同じ
geometry = SCNPlane.planeWithWidth_height_(_width, _height)
geometry.firstMaterial().diffuse().contents = UIColor.color(
red=1.0, green=0.0, blue=0.0, alpha=0.8)
planeNode = SCNNode.nodeWithGeometry_(geometry)
planeNode.eulerAngles = (-pi / 2.0, 0.0, 0.0)
node.addChildNode_(planeNode) # rootNode は、引数_node をインスタンス化したもの
# 2つめ
def renderer_didUpdateNode_forAnchor_(_self, _cmd, renderer, node, anchor):
# 情報が変更(アップデート) された時の処理を指定できる。
# add したパネルのNode の面積情報を更新したりなど
pass
# --- delegate/ delegate 生成時のメソッド作りおわり
# 上記で定義した関数を格納
_methods = [
renderer_didAddNode_forAnchor_,
renderer_didUpdateNode_forAnchor_,
]
_protocols = ['ARSCNViewDelegate']
# 第一引数は、変数名を文字列で指定する
renderer_delegate = create_objc_class(
'renderer_delegate', methods=_methods, protocols=_protocols)
return renderer_delegate.new() # インスタンス化して返す
renderer_didAddNode_forAnchor_
の引数
objc_util
のdelegate
処理の引数は、往々にしてそのままでは使えません。ObjCInstance(引数)
とインスタンス化してから、処理をすることになります。
print
で実体を確認するなどして、変数の状態を確認し適宜対応を変えていきます。
引数の_node
はrootNode
。_anchor
にはトラッキングで取得した情報が入っています。
それぞれObjCInstance
でインスタンス化し、平面情報を取得した際にパネルを呼び出す処理をしています。
renderer_didAddNode_forAnchor_
とrenderer_didUpdateNode_forAnchor_
の処理
サンプルで散見するextent
は非推奨となっていたため、planeExtent
で縦横幅の情報を取得しています。
extent | Apple Developer Documentation
planeExtent | Apple Developer Documentation
パネルは、X軸に面して生成されるので、Z軸に向ける処理をしています。
planeNode.eulerAngles = (-pi / 2.0, 0.0, 0.0)
本来はSCNMatrix4MakeRotation
等の行列処理をおこないたいところですが、objc_util
とMatrix やsimd 関係の構造体の相性があまりよくなく、違う方法で回転をさせています。
まだまだ調査不足の部分ですので、新しい発見がありましたら、追って報告をします。
SCNMatrix4MakeRotation | Apple Developer Documentation
SCNMatrix4 | Apple Developer Documentation
ちなみにARSCNViewDelegate
には、複数のDelegate がありますが、今回は仮に2つ呼び出しをして1つを実装に使っています。
(renderer_didAddNode_forAnchor_
のみでも問題ありません)
.planeDetection = (1 << 0)
指定
def resetTracking(self):
_configuration = ARWorldTrackingConfiguration.new()
_configuration.planeDetection = (1 << 0)
self.sceneView.session().runWithConfiguration_(_configuration)
Tracking の取得をHorizontal(水平)、Vertical(垂直)で指定します。
直接、値(文字列などで)を指定したいところですが、Enumeration Case となっています。
-
ARPlaneDetectionHorizontal | Apple Developer Documentation
ARPlaneDetectionHorizontal = (1 << 0)
-
ARPlaneDetectionVertical | Apple Developer Documentation
ARPlaneDetectionVertical = (1 << 1)
今回は、平面の取得をしたいので水平を選択します。
_configuration.planeDetection = (1 << 0)
次回は
delegate
の事前処理を終えたら、その他は簡単に(delegate
と比較して)実装ができましたかね?
Pythonista3 で「できなさそうなこと」 もお伝えができた点が、個人的に良かったと感じています。
Matrix やsimd に関して、独自実装ではなくobjc_util
でしれっと使う方法を知っている方!ご連絡お待ちしております。
今回までリアカメラを使ったAR でしたので、次回はフロントカメラを使ったフェイストラッキングを紹介したいと思います。
ここまで、読んでいただきありがとうございました。
参考文献
- Apple ARKit by Example. With the beta release of ARKit by… | by Mark Dawson | Mark Dawson
- 動かして学ぶARKit ~平面検出~ | DevelopersIO
- ARKit入門 / 平面の検出(1)|npaka|note
せんでん
Discord
Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。
書籍
iPhone/iPad でプログラミングする最強の本。
その他
- サンプルコード
Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。
コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー
なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー
やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか?って話よ!(クズ)
— pome-ta (@pome_ta93) November 4, 2022
Pythonista3 Advent Calendar 2022 https://t.co/JKUxA525Pt #Qiita
- GitHub
基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。