pythonista3で物理シミュレーション

![IMG_0400.PNG](https://qiita-image-store.s3.amazonaws.com/0/176818/380c8cb0-ab16-e827-a803-183694339c1d.png)
#概要

pythonista3にはObjective-CのクラスをPythonから呼び出すobjc_utilという強力なツールが存在します。
今回は、この機能を使って、scenkitをラッパーして物理シミュレーションを体感してみます。
立方体と球体を出現させて、自由落下させるだけの簡単なデモです。


ソースは以下のとおり。

```python:physics_body.py
# coding: utf-8
from objc_util import *
import ui
import math
import motion
import random
from scene import *
load_framework('SceneKit')
SCNView, SCNScene, SCNBox, SCNSphere,SCNFloor, SCNNode, SCNLight, SCNMaterial, SCNCamera, SCNAction, SCNTransaction,SCNLookAtConstraint,SCNPhysicsShape,SCNPhysicsBody,UIFont= map(ObjCClass, ['SCNView', 'SCNScene', 'SCNBox', 'SCNSphere','SCNFloor', 'SCNNode', 'SCNLight',  'SCNMaterial','SCNCamera', 'SCNAction','SCNTransaction','SCNLookAtConstraint','SCNPhysicsShape','SCNPhysicsBody','UIFont'])
class SCNVector3 (Structure):
    _fields_ = [('x', c_float), ('y', c_float), ('z', c_float)]
W=1
L=1
H=1
LOCALTIME=0
def ObjCMethodPrint(obj):
    print(obj.__class__.__name__)
    if hasattr(obj, '__call__'):
        print(inspect.getargspec(obj))
    else:
        print(dir(obj))
@on_main_thread
class MyScene (Scene):
    def setup(self):
        pass
    def draw(self):
        global LOCALTIME
        LOCALTIME=LOCALTIME+1
        if  (LOCALTIME % 60)==0 and LOCALTIME<=60000:
            sphere= SCNSphere.sphereWithRadius_(1*random.random())
            sphere.material().setColor_(UIColor.greenColor().CGColor())
            sphere_node = SCNNode.nodeWithGeometry_(sphere)
            sphere_node.setPosition_((random.random(), 20*random.random(), random.random()))
            physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(sphere,None)
            physicsBody = SCNPhysicsBody.dynamicBody()
            physicsBody.reflectivity =.9
            sphere_node.physicsBody = physicsBody
            self.root_node.addChildNode_(sphere_node)
      
    def make_view(self,mc):
        main_view_objc = mc
        scene_view = SCNView.alloc().initWithFrame_options_(((0, 0),(100, 100)), None).autorelease()
        scene_view.setAutoresizingMask_(18)
        scene_view.setAllowsCameraControl_(True)
        scene = SCNScene.scene()
        self.root_node = scene.rootNode()  
        #ボックスを生成
        box= SCNBox.boxWithWidth_height_length_chamferRadius_(W, L,H,0)
        box.material().setColor_(UIColor.redColor().CGColor())
        box_node = SCNNode.nodeWithGeometry_(box)
        box_node.setPosition_((0, 8, 0))
        physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(box,None)
        physicsBody = SCNPhysicsBody.dynamicBody()
        physicsBody.reflectivity =.8
        box_node.physicsBody = physicsBody
        self.root_node.addChildNode_(box_node)
        #球体を生成
        sphere= SCNSphere.sphereWithRadius_(0.5)
        sphere.material().setColor_(UIColor.greenColor().CGColor())
        sphere_node = SCNNode.nodeWithGeometry_(sphere)
        sphere_node.setPosition_((0, 10, 0))
        physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(sphere,None)
        physicsBody = SCNPhysicsBody.dynamicBody()
        physicsBody.reflectivity =.9
        sphere_node.physicsBody = physicsBody
        self.root_node.addChildNode_(sphere_node)
        #床を作成
        floor = SCNFloor.floor()
        floor.reflectivity = 0.25
        floor_node= SCNNode.nodeWithGeometry_(floor) 
        floor_node.physicsBody=SCNPhysicsBody.staticBody()
        self.root_node.addChildNode_(floor_node)
        #光を生成
        light_node = SCNNode.node()
        light_node.setPosition_((0, 70, 100))
        light = SCNLight.light()
        light.setType_('omni')
        light.setCastsShadow_(True)
        light.setColor_(UIColor.whiteColor().CGColor())
        light_node.setLight_(light)
        self.root_node.addChildNode_(light_node)
        #カメラをセット
        camera = SCNCamera.camera()
        camera_node = SCNNode.node()
        camera_node.setCamera(camera)
        camera_node.setPosition((0, 1,20))
        self.root_node.addChildNode_(camera_node)
      
        scene_view.setScene_(scene)
        main_view_objc.addSubview_(scene_view) 
if __name__ == "__main__":
    #ObjCMethodPrint(SCNPhysicsBody)
    #set view
    main_view = ui.View()
    main_view_objc = ObjCInstance(main_view)
    main_view.name = 'SceneKit Demo'
    #run MyScene
    my_scene = MyScene()
    scene_view = SceneView()
    scene_view.scene = my_scene
    main_view_objc.addSubview_(scene_view)
    #make scenekit
    my_scene.make_view(main_view_objc)
    #present view
    main_view.present()   
```

Scenekitのメソッドを良く理解せずに適当に使っているので悪しからず。

#物理属性の設定

物理属性としてdynamic,static,kinematicがあり、床にはstaticを、球体とボックスにはdynamicを設定しています。
反発係数のプロパティは、reflectivityで与えます。

#レンダリングループ

レンダリングループさせるためにsceneオブジェクトのdraw()を使っています。
1秒に25回描画してくれるようです。
上記ソースのようにMySceneクラスを作って、以下のようにインスタンスを作って呼び出せば、レンダリングループしてくれます。

```
    #run MyScene
    my_scene = MyScene()
    scene_view = SceneView()
    scene_view.scene = my_scene
    main_view_objc.addSubview_(scene_view)
```

#scenekit viewの作成

SCNViewを使って、viewを作っておきます。
setAllowsCameraControlをTrueにしておくとviewをタップして動かせます。

```
    #make scenekit 
    my_scene.make_view(main_view_objc)
```

```
   def make_view(self,mc):
        main_view_objc = mc
        scene_view = SCNView.alloc().initWithFrame_options_(((0, 0),(400, 400)), None).autorelease()
        scene_view.setAutoresizingMask_(18)
        scene_view.setAllowsCameraControl_(True)
```

#ボックス、球体、床、ライト、カメラの配置

root_nodeを作って、addChildNode_メソッドを使ってボックス、球体、床、ライト、カメラをぶら下げていきます。


```
        scene = SCNScene.scene()
        self.root_node = scene.rootNode()  
```

#ボックス、球体の生成

ボックスはboxWithWidth_height_length_chamferRadius_プロパティで幅、長さ、高さ、角の丸みを指定します。
球体は、sphereWithRadius_プロパティで球体の半径を指定します。
material().setColor_で色を指定します。
物理属性は、physicsBodyプロパティで指定します。
最後にaddChildNodeメソッドでroot_nodeにぶら下げておきます。


```
        #ボックスを生成
        box= SCNBox.boxWithWidth_height_length_chamferRadius_(W, L,H,0)
        box.material().setColor_(UIColor.redColor().CGColor())
        box_node = SCNNode.nodeWithGeometry_(box)
        box_node.setPosition_((0, 8, 0))
        physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(box,None)
        physicsBody = SCNPhysicsBody.dynamicBody()
        physicsBody.reflectivity =.8
        box_node.physicsBody = physicsBody
        self.root_node.addChildNode_(box_node)
        #球体を生成
        sphere= SCNSphere.sphereWithRadius_(0.5)
        sphere.material().setColor_(UIColor.greenColor().CGColor())
        sphere_node = SCNNode.nodeWithGeometry_(sphere)
        sphere_node.setPosition_((0, 10, 0))
        physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(sphere,None)
        physicsBody = SCNPhysicsBody.dynamicBody()
        physicsBody.reflectivity =.9
        sphere_node.physicsBody = physicsBody
        self.root_node.addChildNode_(sphere_node)
```

#床の作成

床は、SCNFloor.floor()で作成します。
動かないようにstatic属性を与えておきます。

```
        #床を作成
        floor = SCNFloor.floor()
        floor.reflectivity = 0.25
        floor_node= SCNNode.nodeWithGeometry_(floor) 
        floor_node.physicsBody=SCNPhysicsBody.staticBody()
        self.root_node.addChildNode_(floor_node)
```

#ライトの生成

ライトはSCNLight.light()で生成します。
ライトのタイプはspot,omni,directional,ambientが存在選べます。

```
        #光を生成
        light_node = SCNNode.node()
        light_node.setPosition_((0, 70, 100))
        light = SCNLight.light()
        light.setType_('omni')
        light.setCastsShadow_(True)
        light.setColor_(UIColor.whiteColor().CGColor())
        light_node.setLight_(light)
        self.root_node.addChildNode_(light_node)
```

#カメラの配置

カメラは、SCNCamera.camera()で生成します。
セットした物体が写るようにカメラを配置します。
写る位置を探すのは結構めんどくさいです。

```
        #カメラをセット
        camera = SCNCamera.camera()
        camera_node = SCNNode.node()
        camera_node.setCamera(camera)
        camera_node.setPosition((0, 1,20))
        self.root_node.addChildNode_(camera_node)
```

#物体の追加生成

60描画毎に球体を追加生成しています。
LOCALTIME<=60000で打ち止めにしています。
同じ位置から落下させると上に乗ってしまうので、出現位置をランダムに変えています。


```
        def draw(self):
        global LOCALTIME
        LOCALTIME=LOCALTIME+1
        if  (LOCALTIME % 60)==0 and LOCALTIME<=60000:
            sphere= SCNSphere.sphereWithRadius_(1*random.random())
            sphere.material().setColor_(UIColor.greenColor().CGColor())
            sphere_node = SCNNode.nodeWithGeometry_(sphere)
            sphere_node.setPosition_((random.random(), 20*random.random(), random.random()))
            physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(sphere,None)
            physicsBody = SCNPhysicsBody.dynamicBody()
            physicsBody.reflectivity =.9
            sphere_node.physicsBody = physicsBody
            self.root_node.addChildNode_(sphere_node)
```

#速度の追加

初期速度を追加は、physicsBody.velocity=((0,0,-100))のようにx,y,z方向の速度を与えるだけです。
以下少し量を増やした物理シミュレーションを示します。青いボールとオレンジのボールが横から飛んできます。
いろいろ設定を変えて楽しめます。
![IMG_0411.PNG](https://qiita-image-store.s3.amazonaws.com/0/176818/086b07fa-4f3e-eedf-a33a-761be2e745e5.png)

```
# coding: utf-8
from objc_util import *
import ui
import math
import motion
import random
from scene import *
load_framework('SceneKit')
SCNView, SCNScene, SCNBox, SCNSphere,SCNFloor, SCNNode, SCNLight, SCNMaterial, SCNCamera, SCNAction, SCNTransaction,SCNLookAtConstraint,SCNPhysicsShape,SCNPhysicsBody,UIFont= map(ObjCClass, ['SCNView', 'SCNScene', 'SCNBox', 'SCNSphere','SCNFloor', 'SCNNode', 'SCNLight',  'SCNMaterial','SCNCamera', 'SCNAction','SCNTransaction','SCNLookAtConstraint','SCNPhysicsShape','SCNPhysicsBody','UIFont'])
class SCNVector3 (Structure):
    _fields_ = [('x', c_float), ('y', c_float), ('z', c_float)]
W=1
L=1
H=1
LOCALTIME=0
def ObjCMethodPrint(obj):
    print(obj.__class__.__name__)
    if hasattr(obj, '__call__'):
        print(inspect.getargspec(obj))
    else:
        print(dir(obj))
@on_main_thread
class MyScene (Scene):
    def setup(self):
        pass
    def draw(self):
        global LOCALTIME
        LOCALTIME=LOCALTIME+1
        if  (LOCALTIME % 10)==0 and LOCALTIME<=6000:
            if random.random()<=0.5 :
                makeobject= SCNSphere.sphereWithRadius_(1*random.random())
            else:
                makeobject= SCNBox.boxWithWidth_height_length_chamferRadius_(2*W*random.random(), 2*L*random.random(),2*H*random.random(),0)
            makeobject.material().setColor_(UIColor.greenColor().CGColor())
            object_node = SCNNode.nodeWithGeometry_(makeobject)
            object_node.setPosition_((random.random(), 20*random.random(), random.random()))
            physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(makeobject,None)
            physicsBody = SCNPhysicsBody.dynamicBody()
            physicsBody.reflectivity =.9
            physicsBody.velocity=((30*(0.5-random.random()),30*(0.5-random.random()),30*(0.5-random.random())))
            object_node.physicsBody = physicsBody
            object_node.runAction_(SCNAction.rotateToX_y_z_duration_(math.pi*random.random(),math.pi*random.random(),math.pi*random.random(),0))
            self.root_node.addChildNode_(object_node)
            
      
    def make_view(self,mc):
        main_view_objc = mc
        scene_view = SCNView.alloc().initWithFrame_options_(((0, 0),(100, 100)), None).autorelease()
        scene_view.setAutoresizingMask_(18)
        scene_view.setAllowsCameraControl_(True)
        scene = SCNScene.scene()
        self.root_node = scene.rootNode()  
        #ボックスを生成
        for i in range(99):
            cube=SCNBox.boxWithWidth_height_length_chamferRadius_(W, L,H,0)
            cube.material().setColor_(UIColor.yellowColor().CGColor())
            cube_node = SCNNode.nodeWithGeometry_(cube)
            cube_node.setPosition_((W*(i%10), L*(int(i/10)), -H*20))
            physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(cube,None)
            physicsBody = SCNPhysicsBody.dynamicBody()
            physicsBody.reflectivity =.8
            cube_node.physicsBody = physicsBody
            self.root_node.addChildNode_(cube_node)
        for i in range(100):    
            box= SCNBox.boxWithWidth_height_length_chamferRadius_(W, L,H,0)
            box.material().setColor_(UIColor.redColor().CGColor())
            box_node = SCNNode.nodeWithGeometry_(box)
            box_node.setPosition_((8*random.random(), 100*random.random(), -8*random.random()))
            box_node.runAction_(SCNAction.rotateToX_y_z_duration_(math.pi*random.random(),math.pi*random.random(),math.pi*random.random(),0))
            physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(box,None)
            physicsBody = SCNPhysicsBody.dynamicBody()
            physicsBody.reflectivity =.8
            box_node.physicsBody = physicsBody
            self.root_node.addChildNode_(box_node)
        #球体を生成
        sphere= SCNSphere.sphereWithRadius_(1.5)
        #ObjCMethodPrint(sphere)
        sphere.material().setColor_(UIColor.blueColor().CGColor())
        sphere_node = SCNNode.nodeWithGeometry_(sphere)
        sphere_node.setPosition_((L*5, H*12, 150))
        physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(sphere,None)
        physicsBody = SCNPhysicsBody.dynamicBody()
        physicsBody.reflectivity =.9
        physicsBody.velocity=((0,0,-150))
        #ObjCMethodPrint(physicsBody)
        sphere_node.physicsBody = physicsBody
        self.root_node.addChildNode_(sphere_node)
        #球体を生成 2個め
        sphere= SCNSphere.sphereWithRadius_(2)
        #ObjCMethodPrint(sphere)
        sphere.material().setColor_(UIColor.orangeColor().CGColor())
        sphere_node = SCNNode.nodeWithGeometry_(sphere)
        sphere_node.setPosition_((L*5, H*40, 200))
        physicsShape = SCNPhysicsShape.shapeWithGeometry_options_(sphere,None)
        physicsBody = SCNPhysicsBody.dynamicBody()
        physicsBody.reflectivity =.9
        physicsBody.velocity=((0,0,-100))
        #ObjCMethodPrint(physicsBody)
        sphere_node.physicsBody = physicsBody
        self.root_node.addChildNode_(sphere_node)
        #床を作成
        floor = SCNFloor.floor()
        floor.reflectivity = 0.25
        floor_node= SCNNode.nodeWithGeometry_(floor) 
        floor_node.physicsBody=SCNPhysicsBody.staticBody()
        self.root_node.addChildNode_(floor_node)
        #光を生成
        light_node = SCNNode.node()
        light_node.setPosition_((0, 70, 100))
        light = SCNLight.light()
        light.setType_('omni')
        light.setCastsShadow_(True)
        light.setColor_(UIColor.whiteColor().CGColor())
        light_node.setLight_(light)
        self.root_node.addChildNode_(light_node)
        #カメラをセット
        camera = SCNCamera.camera()
        camera_node = SCNNode.node()
        camera_node.setCamera(camera)
        camera_node.setPosition((-20, 1,20))
        camera_node.setRotation_((0, 1, 0, -math.pi*1/3))
        self.root_node.addChildNode_(camera_node)
      
        scene_view.setScene_(scene)
        main_view_objc.addSubview_(scene_view) 
if __name__ == "__main__":
    #ObjCMethodPrint(SCNPhysicsBody)
    #set view
    main_view = ui.View()
    main_view_objc = ObjCInstance(main_view)
    main_view.name = 'SceneKit Demo'
    #run MyScene
    my_scene = MyScene()
    scene_view = SceneView()
    scene_view.scene = my_scene
    main_view_objc.addSubview_(scene_view)
    #make scenekit
    my_scene.make_view(main_view_objc)
    #present view
    main_view.present()  
```