この記事は、Pythonista3 Advent Calendar 2022 の17日目の記事です。
一方的な偏った目線で、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 です。
この記事でわかること
- Pythonista3 での基礎的なsceneKit の使い方
- 球体やボックスを出す
- カメラを操作する
- 物理演算を使う
- debugOptions を使う
- Swift -> Objective-C ->
objc_util
モジュールへの書き換え
Pythonista3 のscene
モジュールじゃないの。SceneKit Framework なの
混同してしまいますが(過去の私だけ?)、2D アニメーションやゲームに特化した、scene — 2D Games and Animations — Python 3.6.1 documentation モジュールではなく。
3DCG のFramework である、SceneKit | Apple Developer Documentation をobjc_util
モジュールで呼び出してPythonista3 で遊んでいきます。
Qiita に他の方が書かれた記事もあります。
PythonistaでSceneKitを使った3D描画 - Qiita
pythonista3で物理シミュレーション - Qiita
SceneKit(Swift やObjective-C で書かれた)コードから、Pythonista3 へ実装できるよう進めていきます。
当たり前ですが、Pythonista3 のSceneKit 実装のコードよりもSwift やObjective-C で書かれたSceneKit のサンプルの方が多いので。
前回までのAVAudioEngine
は地獄でしたが、今回は比較的簡単だと思います!
基本的な考え方
SCNScene | Apple Developer Documentation
Overview のイラストにあるように、描画するためのView(SCNView | Apple Developer Documentation)があり、3DCG 世界の土台としてのScene(SCNScene | Apple Developer Documentation)があります。
我々は、View を通して3DCG の世界を覗かせて頂いています。
Scene にはrootNode
というものが生えています。我々が3DCG 世界に登場させたいモノは、Node
というカタチに納めてrootNode
に取り込んでもらいます(addChildNode
)。
SCNView → UIView にaddSubview することで見れる
|
SCNScene
|
rootNode
↑ ↑ ↑ ↑ addChildNode(Node)
さまざまなNode たち
Node(SCNNode | Apple Developer Documentation)は、さまざまなモノを取り付けられます。
- ジオメトリ(3D の物体オブジェクト、メッシュ)
- 色情報や質感
- ライト
- カメラ
またNode 自体では
- (3DCG内の)位置
- 回転
- スケール
の情報を持ち操作をしていきます。
Node に出したいモノや使いたいモノを付けて、位置や大きさなどを決め、rootNode
へaddChildNode
することで、3DCG 世界に登場させることができるのです。
UIView のaddSubview
で他のView を取り込んで画面を構築していく親子関係と、大きくは変わりません。
ちなみに、SceneKit は右手座標系です。OpenGL, Vulkan と同じ座標系です。
ちなみにちなみに、Metal は、左手系なのでDirectX と同じですね。
参照先
こちらのSceneKit
カテゴリに大変お世話になっています。
SceneKit カテゴリーの記事一覧 - Apple Engine
しかし更新終了のお知らせ - Apple Engineと、あるように今後消えてしまう可能性もあるので、読めるうちに読んでおきましょう。
今回は「iOS で SceneKit を試す(Swift 3) 」シリーズ参考に進めていきます。
その1 〜その3 の概要は、先ほどの私の説明よに数億倍わかりやすいのでおすすめです。
まずは、iOS で SceneKit を試す(Swift 3) その4 - SceneKit の構造 - Apple Engine より始めていきます。都度対応するパートも提示するので、Swift のコード等は、リンク先を参照ください。
Pythonista3 に実装
土台つくり
Pythonista3 のui.View
にSceneKit を出せるようにしましょう。
その3、その4を参考にしています。
iOS で SceneKit を試す(Swift 3) その4 - SceneKit の構造 - Apple Engine
iOS で SceneKit を試す(Swift 3) その5 - シーンエディタを使用しない空のテンプレートをつくる - Apple Engine
from objc_util import load_framework, ObjCClass, on_main_thread
from objc_util import UIColor
import ui
load_framework('SceneKit')
SCNScene = ObjCClass('SCNScene')
SCNView = ObjCClass('SCNView')
class GameScene:
def __init__(self):
self.scene: SCNScene
self.setUpScene()
def setUpScene(self):
scene = SCNScene.scene()
# ---
# ここに処理を書いていく
# ---
self.scene = scene
class View(ui.View):
def __init__(self, *args, **kwargs):
ui.View.__init__(self, *args, **kwargs)
self.name = '土台作り'
self.bg_color = 'maroon'
self.scene: GameScene
self.scnView: SCNView
self.viewDidLoad()
self.objc_instance.addSubview_(self.scnView)
#@on_main_thread
def viewDidLoad(self):
scene = GameScene()
# --- SCNView
_frame = ((0, 0), (100, 100))
scnView = SCNView.alloc().initWithFrame_(_frame)
# ui.View.flex = 'WH' と同じ
scnView.setAutoresizingMask_((1 << 1) | (1 << 4))
scnView.backgroundColor = UIColor.blackColor()
scnView.showsStatistics = True
scnView.autorelease()
scnView.scene = scene.scene
self.scene = scene
self.scnView = scnView
def touch_began(self, touch):
pass
if __name__ == '__main__':
view = View()
view.present(style='fullscreen', orientations=['portrait'])
ようこそ!SceneKit の世界へ!
デバッグ情報を出すのになかなかコツがいるのですが、デバッグ情報のバーの左側に+
のアイコンがあり、その近辺をポチポチしてると出てきます。。。
ui.View
とSCNView
の繋ぎ合わせ
Pythonista3 の世界にobjc_util
で呼び出したSceneKit を繋げるために、SCNView
を介しています。
ui
モジュールのView とobjc_util
のView は、気軽には繋げません。
ui.View.objc_instance.addSubview_(scnView)
ui.View
側で、objc_util
のView として立ち回れるobjc_instance
として、Objective-C のUIView
のメソッドのaddSubview_
を使いSCNView
を取り込んでいます。
SCNView
の事前準備
View なので、サイズ等を指定しなければならないのですが、ui.View
よりも少し手間ですが、設定します。
SCNView = ObjCClass('SCNView')
_frame = ((0, 0), (100, 100))
scnView = SCNView.alloc().initWithFrame_(_frame)
# ui.View.flex = 'WH' と同じ
scnView.setAutoresizingMask_((1 << 1) | (1 << 4))
frame
を((位置x: 0, 位置y: 0), (横幅: 100, 縦幅: 100))
として、仮で決めています。
次のsetAutoresizingMask_
が肝ですが、ui.View
のflex
のように画面設定をしています。WH
幅と高さを最大。ということですね。
(1 << 1) | (1 << 4)
は、整数値で18
です。直接18
と入れても機能します。
面白いですね。
この準備を終えたら、ui.View
のView にaddSubview_
してもらい、晴れてPythonista3 で描画されます。
ここでしれっと、背景を黒色にしています。
scnView.backgroundColor = UIColor.blackColor()
デコレータ@on_main_thread
ui
の処理にブロックされずに、デコレータの部分も処理をしてくれます。
実行時にui.View
で設定した赤の背景が見えてからSCNView
が表示されていました。
上記のコードではコメントアウトしています。
コメントアウトを外し実行すると、赤背景は出ずに、立ち上がり直後SCNView
の黒画面が表示されるのが確認できます。
アプリとしての格好はいいのですが、on_main_thread
を使うことでエラー箇所が見つかり辛いこともあります。
今回はその程度ですが、他の事例では「ui.View
であれして、objc_util
でこれして、、、」とジャグリング状態になり、必須の場面も出てきます。
class GameScene
往々にしてSceneKit は、class 内がfat になりがちです。
View の処理と、Node の処理を意識的に分ける意味合いでGameScene
class として宣言しています。
View 側で、scene
を触りたい場面には、self
を生やしていく方針です。
箱を出して色付け、ライト設置で影も付けるし、カメラ登場の3D 空間ぐりんぐりんする
何もない3D 空間から、ドカっと登場させます。
- 赤色
ambient
- boxに青色
from objc_util import load_framework, ObjCClass, on_main_thread
from objc_util import UIColor
import ui
import pdbg
load_framework('SceneKit')
SCNScene = ObjCClass('SCNScene')
SCNView = ObjCClass('SCNView')
SCNNode = ObjCClass('SCNNode')
SCNLight = ObjCClass('SCNLight')
SCNCamera = ObjCClass('SCNCamera')
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_(2, 2, 2, 0.2)
#box.firstMaterial().diffuse().contents = UIColor.blueColor()
geometryNode = SCNNode.nodeWithGeometry_(box)
geometryNode.runAction_(
SCNAction.repeatActionForever_(
SCNAction.rotateByX_y_z_duration_(0.0, 0.2, 0.1, 0.3)))
scene_rootNode_addChildNode_(geometryNode)
# --- SCNLight
lightNode = SCNNode.node()
lightNode.light = SCNLight.light()
lightNode.position = (0.0, 10.0, 10.0)
scene_rootNode_addChildNode_(lightNode)
ambientLightNode = SCNNode.node()
ambientLightNode.light = SCNLight.light()
ambientLightNode.light().type = 'ambient'
ambientLightNode.light().color = UIColor.redColor()
#ambientLightNode.light().color = UIColor.darkGrayColor()
scene_rootNode_addChildNode_(ambientLightNode)
# --- SCNCamera
cameraNode = SCNNode.node()
cameraNode.camera = SCNCamera.camera()
cameraNode.position = (0.0, 0.0, 10.0)
scene_rootNode_addChildNode_(cameraNode)
self.scene = scene
class View(ui.View):
def __init__(self, *args, **kwargs):
ui.View.__init__(self, *args, **kwargs)
self.name = ''
self.bg_color = 'maroon'
self.scene: GameScene
self.scnView: SCNView
self.viewDidLoad()
self.objc_instance.addSubview_(self.scnView)
#@on_main_thread
def viewDidLoad(self):
scene = GameScene()
# --- SCNView
_frame = ((0, 0), (100, 100))
scnView = SCNView.alloc().initWithFrame_(_frame)
# ui.View.flex = 'WH' と同じ
scnView.setAutoresizingMask_((1 << 1) | (1 << 4))
scnView.backgroundColor = UIColor.blackColor()
scnView.allowsCameraControl = True
scnView.showsStatistics = True
scnView.autorelease()
scnView.scene = scene.scene
self.scene = scene
self.scnView = scnView
def touch_began(self, touch):
pass
if __name__ == '__main__':
view = View()
view.present(style='fullscreen', orientations=['portrait'])
引き続きその3、その4を参考にしています。
class GameScene
の部分
setUpScene
内で、たくさん登場させています。
上から見ていきましょう。
基本的にオブジェクトは、alloc.init
(new
)せずに呼び出せるのが特徴です。
scene.rootNode().addChildNode_
の呼び出し
生成した、Node たちを3DCG 世界(scene
)に登場させる、クサビ的な立ち位置です。
Node は、rootNode
に全集合させます。
毎回、scene.rootNode().addChildNode_
と入力するのが面倒なので、scene_rootNode_addChildNode_
と変数化しています。
あまり望ましい方法ではありませんが、scene.rootNode()
と.
を繋ぐ場合でもインスタンス化しなければならず、その点が(私的に)罠だったりするので変数化しています。
気持ち悪い場合には、素直に:
scene.rootNode().addChildNode_(Node)
で良いと思います。
SCNBox
SCNBox.boxWithWidth_height_length_chamferRadius_
にて、サイズと角丸を指定したGeometry
を生成しています。
色を付けるには、firstMaterial().diffuse().contents
よりUIColor
を使います。
マテリアルの細かい質感は、iOS で SceneKit を試す(Swift 3) その33 - ジオメトリの質感を決めるマテリアルについて - Apple Engine こちらにて、説明があります。
rootNode
に取り込んでもらうために、SCNNode.nodeWithGeometry_(box)
として、ジオメトリのbox
をSCNNode
に格納します。
SCNAction
で、Node にアニメーション設定をして、今回は常にくるくると回ってもらうことにしています。
iOS で SceneKit を試す(Swift 3) その4 - SceneKit の構造 - Apple Engine では、地球のテクスチャを貼り付けていますが、外部データ読み込みは次回説明予定なので、今回は、球体ではなくBox に回ってもらうことにしています。
SCNLight
, SCNCamera
ジオメトリ生成とほぼ同様です。最終的にSCNNode
へNode として、存在していないとrootNode
へaddChildNode_
できない点を忘れずに意識します。
Swift コードですと:
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
と、class 直接の呼び出しです。
objc_util
(Objective-C)ですと、ObjCClass
からメソッドを呼び出す一手間が必要です:
cameraNode = SCNNode.node()
cameraNode.camera = SCNCamera.camera()
camera | Apple Developer Documentation
Documentation や、Pythonista3 上でのprint
デバッグなどをして確認します。
視点を動かす
SCNView
に、.allowsCameraControl = True
とすることで画面を動かしたり、ピンチインアウトで拡大縮小ができます:
scnView.allowsCameraControl = True
ダブルタップすると、カメラの位置に戻ります。
設置したSCNCamera
を動かしているのではなく、カメラから「幽体離脱」的に抜け出して傍観者モードのようになるみたいです。
この世界に重力を導入することとする
その9とその10を参考にしながら、物理演算を設定しボールを落下させたり衝突させたりしましょう。
iOS で SceneKit を試す(Swift 3) その9 - 物理アニメーションを試す - Apple Engine
iOS で SceneKit を試す(Swift 3) その10 - ノードをコピーして端末負荷を下げる - Apple Engine
from objc_util import load_framework, ObjCClass, on_main_thread
from objc_util import UIColor
import ui
import pdbg
load_framework('SceneKit')
SCNScene = ObjCClass('SCNScene')
SCNView = ObjCClass('SCNView')
SCNNode = ObjCClass('SCNNode')
SCNLight = ObjCClass('SCNLight')
SCNCamera = ObjCClass('SCNCamera')
SCNAction = ObjCClass('SCNAction')
'''
Static = 0
Dynamic = 1
Kinematic = 2
'''
SCNPhysicsBody = ObjCClass('SCNPhysicsBody')
SCNPhysicsShape = ObjCClass('SCNPhysicsShape')
SCNSphere = ObjCClass('SCNSphere')
SCNFloor = ObjCClass('SCNFloor')
class GameScene:
def __init__(self):
self.scene: SCNScene
self.setUpScene()
def setUpScene(self):
scene = SCNScene.scene()
# 呼び出しが面倒なので、変数化
scene_rootNode_addChildNode_ = scene.rootNode().addChildNode_
# --- SCNFloor
floor = SCNFloor.floor()
floorNode = SCNNode.nodeWithGeometry_(floor)
floorNode.position = (0.0, -4.0, 0.0)
floorNode.eulerAngles = (-0.001, 0.0, 0.0)
floorNode.physicsBody = SCNPhysicsBody.bodyWithType_shape_(0, None)
scene_rootNode_addChildNode_(floorNode)
# --- SCNSphere
ball = SCNSphere.sphereWithRadius_(0.5)
ballNode = SCNNode.nodeWithGeometry_(ball)
ballNode.position.y = 2
physicsBall = SCNPhysicsShape.shapeWithGeometry_options_(ball, None)
#physicsBall = SCNPhysicsShape.shapeWithNode_options_(ballNode, None)
ballNode.physicsBody = SCNPhysicsBody.bodyWithType_shape_(1, physicsBall)
scene_rootNode_addChildNode_(ballNode)
# --- SCNLight
lightNode = SCNNode.node()
lightNode.light = SCNLight.light()
lightNode.position = (0.0, 10.0, 10.0)
scene_rootNode_addChildNode_(lightNode)
# --- SCNCamera
cameraNode = SCNNode.node()
cameraNode.camera = SCNCamera.camera()
cameraNode.position = (0.0, 0.0, 10.0)
scene_rootNode_addChildNode_(cameraNode)
self.scene = scene
self.scene_rootNode_addChildNode_ = scene_rootNode_addChildNode_
self.ballNode = ballNode
class View(ui.View):
def __init__(self, *args, **kwargs):
ui.View.__init__(self, *args, **kwargs)
self.name = ''
self.bg_color = 'maroon'
self.scene: GameScene
self.scnView: SCNView
self.viewDidLoad()
self.objc_instance.addSubview_(self.scnView)
#@on_main_thread
def viewDidLoad(self):
scene = GameScene()
# --- SCNView
_frame = ((0, 0), (100, 100))
scnView = SCNView.alloc().initWithFrame_(_frame)
# ui.View.flex = 'WH' と同じ
scnView.setAutoresizingMask_((1 << 1) | (1 << 4))
scnView.backgroundColor = UIColor.blackColor()
scnView.allowsCameraControl = True
scnView.showsStatistics = True
'''
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)
'''
_debugOptions = ((1 << 0) | (1 << 1) | (1 << 4) | (1 << 10))
scnView.debugOptions = _debugOptions
scnView.autorelease()
scnView.scene = scene.scene
self.scene = scene
self.scnView = scnView
def touch_began(self, touch):
ballNode = self.scene.ballNode.clone()
self.scene.scene_rootNode_addChildNode_(ballNode)
if __name__ == '__main__':
view = View()
view.present(style='fullscreen', orientations=['portrait'])
床に少し傾斜をつけています、微妙に角度があればよかったので適当に設定しています。
physicsBall
に対しGeometry
かNode
どちらがいいかわらず、現在検証中です。
また、その10のclone
が、多分効いていない状態だと思われます。こちらも調査中です。。。
デバッグオブションを設定して3DCG 世界をもっと深く見る
個人的にはテンションあがるやつです。
上がる下がるの問題ではなく、ジオメトリのワイヤーやライトの位置。物理計算の範囲などの情報を可視化してくれます。
SCNDebugOptions | Apple Developer Documentation
View の時のsetAutoresizingMask
に似た呼び出し方です。
_debugOptions = ((1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 10))
scnView.debugOptions = _debugOptions
ワイヤー表示っていいですよね。
次回は
Pythonista3 で SceneKit Framework を呼び出し、3DCG 世界を体験してみました。
案外、サンプルコードの読み替えで実装できることを知っていただけましたら嬉しいです。
SceneKit とobjc_util
の関係性の理解が深まったら、以下リポジトリのコードを読んでみるのもおすすめです。
今回の我々のように、生なobjc_util
を使うのではなく、Wrapper としてPythonista3 で気軽にSceneKit が使えるように作成した方がいらっしゃいます。 なんという情熱なんでしょう。。。
Swift やObjective-C で書かれたサンプル実装で困った時に、該当の実装内容を見にいくとヒントがあったりして勉強になります。
次回は、もっと素敵な絵を出したいので、外部からデータを持ってきてSceneKit 上に登場させたりしたいと思います。
絵力がグッと上がると思いますよー。
ここまで、読んでいただきありがとうございました。
せんでん
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 にコードをあげているので、何にハマって何を実装しているのか観測できると思います。