0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonista3Advent Calendar 2022

Day 17

Pythonista3 で3DCG やろうぜ!SceneKit のプリミティブなもので遊ぶ

Last updated at Posted at 2022-12-16

この記事は、Pythonista3 Advent Calendar 2022 の17日目の記事です。

一方的な偏った目線で、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 です。

この記事でわかること

  • Pythonista3 での基礎的なsceneKit の使い方
    • 球体やボックスを出す
    • カメラを操作する
    • 物理演算を使う
    • debugOptions を使う
  • Swift -> Objective-C -> objc_util モジュールへの書き換え

img221208_175438.gif

img221209_004533.gif

Pythonista3 のscene モジュールじゃないの。SceneKit Framework なの

混同してしまいますが(過去の私だけ?)、2D アニメーションやゲームに特化した、scene — 2D Games and Animations — Python 3.6.1 documentation モジュールではなく。

3DCG のFramework である、SceneKit | Apple Developer Documentationobjc_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 に出したいモノや使いたいモノを付けて、位置や大きさなどを決め、rootNodeaddChildNode することで、3DCG 世界に登場させることができるのです。

UIView のaddSubview で他のView を取り込んで画面を構築していく親子関係と、大きくは変わりません。

ちなみに、SceneKit は右手座標系です。OpenGL, Vulkan と同じ座標系です。

ちなみにちなみに、Metal は、左手系なのでDirectX と同じですね。

参照先

Apple Engine

こちらの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

img221207_202303.gif

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 の世界へ!

デバッグ情報を出すのになかなかコツがいるのですが、デバッグ情報のバーの左側に+ のアイコンがあり、その近辺をポチポチしてると出てきます。。。

img221207_203225.png

ui.ViewSCNView の繋ぎ合わせ

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.Viewflex のように画面設定をしています。WH 幅と高さを最大。ということですね。

(1 << 1) | (1 << 4) は、整数値で18 です。直接18 と入れても機能します。

面白いですね。

この準備を終えたら、ui.View のView にaddSubview_ してもらい、晴れてPythonista3 で描画されます。

ここでしれっと、背景を黒色にしています。

scnView.backgroundColor = UIColor.blackColor()

デコレータ@on_main_thread

ui の処理にブロックされずに、デコレータの部分も処理をしてくれます。

実行時にui.View で設定した赤の背景が見えてからSCNView が表示されていました。

img221207_210945.png

上記のコードではコメントアウトしています。

コメントアウトを外し実行すると、赤背景は出ずに、立ち上がり直後SCNView の黒画面が表示されるのが確認できます。

アプリとしての格好はいいのですが、on_main_thread を使うことでエラー箇所が見つかり辛いこともあります。

今回はその程度ですが、他の事例では「ui.View であれして、objc_util でこれして、、、」とジャグリング状態になり、必須の場面も出てきます。

class GameScene

往々にしてSceneKit は、class 内がfat になりがちです。

View の処理と、Node の処理を意識的に分ける意味合いでGameScene class として宣言しています。

View 側で、scene を触りたい場面には、self を生やしていく方針です。

箱を出して色付け、ライト設置で影も付けるし、カメラ登場の3D 空間ぐりんぐりんする

何もない3D 空間から、ドカっと登場させます。

  • 赤色ambient

img221208_175016.gif

  • boxに青色

img221208_175438.gif

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.initnew)せずに呼び出せるのが特徴です。

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) として、ジオメトリのboxSCNNode に格納します。

SCNAction で、Node にアニメーション設定をして、今回は常にくるくると回ってもらうことにしています。

iOS で SceneKit を試す(Swift 3) その4 - SceneKit の構造 - Apple Engine では、地球のテクスチャを貼り付けていますが、外部データ読み込みは次回説明予定なので、今回は、球体ではなくBox に回ってもらうことにしています。

SCNLight, SCNCamera

ジオメトリ生成とほぼ同様です。最終的にSCNNode へNode として、存在していないとrootNodeaddChildNode_ できない点を忘れずに意識します。

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

img221209_004533.gif

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 に対しGeometryNode どちらがいいかわらず、現在検証中です。

また、その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

img221209_005508.png

ワイヤー表示っていいですよね。

次回は

Pythonista3 で SceneKit Framework を呼び出し、3DCG 世界を体験してみました。

案外、サンプルコードの読み替えで実装できることを知っていただけましたら嬉しいです。

SceneKit とobjc_util の関係性の理解が深まったら、以下リポジトリのコードを読んでみるのもおすすめです。

pulbrich/sceneKit-wrapper-for-Pythonista: SceneKit module access from Pythonista, pure python package using objc_util

今回の我々のように、生なobjc_util を使うのではなく、Wrapper としてPythonista3 で気軽にSceneKit が使えるように作成した方がいらっしゃいます。 なんという情熱なんでしょう。。。

Swift やObjective-C で書かれたサンプル実装で困った時に、該当の実装内容を見にいくとヒントがあったりして勉強になります。

次回は、もっと素敵な絵を出したいので、外部からデータを持ってきてSceneKit 上に登場させたりしたいと思います。

絵力がグッと上がると思いますよー。

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

せんでん

Discord

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

書籍

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

その他

  • サンプルコード

Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。

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

  • Twitter

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

  • GitHub

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?