PyODE チュートリアル 2

PyODEのサンプル実行例
-



[本家 PyODE 様サイト](http://pyode.sourceforge.net/) の「Tutorial 2」を実行してみました。

--
他のチュート実施記事はこちら ↓
　　　・[「Tutorial 1」](https://qiita.com/emuai/items/75df831c16dbe660df68)（落下物体、Matplotlib描画を追加）
　　　・[「Tutorial 3」](https://qiita.com/emuai/items/840c776f146ac12ac416)（多体接触、OpenGL）

--

本題の「Tutorial 2」ですが、
元の内容は
　・**3個のオブジェクト**をヒンジ2個でつないで振り子を構成。
　・1個のヒンジに角速度/トルクを設定
　・可視化は **Pygame による 2D 描画**。

ここでは角速度の設定はコメントアウトしました。
また、画面サイズや色づけなど変更して GIF 動画出力してみました。

##環境など
・Win10
・Python 3.8.0　64bit
・PyODE 0.15.2
・Pygame 1.9.6
・Pillow 7



##「Tutorial 2」をそのまま実行する際の修正
筆者環境では PyODE サイトのコードそのままでは実行エラーした。

```:pygameエラーメッセージ抜粋
pygame.draw.circle(srf, rgb, coord( xyz ), 20, 0)
TypeError: integer argument expected, got float
```

対処：
　　コードの冒頭にあるcoord()というルーチンで、
　　return される値2つをそれぞれ int() で囲いました。

```python:pygameエラー対処
def coord(x,y):
    "Convert world coordinates to pixel coordinates."
    return int(320+170*x), int(400-170*y)
```


###結果
実行すると Pygame による2D動画表が表示される。
ここでは GIF 形式でファイル保存したものを掲載します。
![tutorial_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469875/13647f8c-664c-5c8d-d3d2-c92d6423af6c.gif)

ODEのオブジェクトは3個の 〇 だけです。
線は描画しているだけ。
黒いオブジェクトは空間に固定されています。（このオブジェクトは無くてもよいのです。）
ジョイント設定、描画設定等には変更加えてあります。


###コード（全部）

不要部分とか残っていますが
小さいコードなので丸ごと載せときます。

```python:Python3
# pyODE example 2: Connecting bodies with joints
##  Modified: Window-sizing, jointing-condition, and others.

import pygame
from pygame.locals import *
import ode


W = 640
H = 640
CX = 320
CY = 320
S = 80.
def coord(xyz):
    (x,y,z) = xyz
    "Convert world coordinates to pixel coordinates."
    return int( CX +S*x ), int( CY -S*y)



from PIL import Image, ImageDraw

def storeImage( srfc, images ):
    if NI_MAX <= len(images):
        return
    s = srfc
    buf = s.get_buffer()
    im = Image.frombytes( "RGBA", s.get_size(), buf.raw )
    B,G,R,A = im.split()
    img = Image.merge("RGBA",(R,G,B,A))
    images.append(img)

def gif(images):
    print(' @ gif(), ')
    savepath = 'tutorial_2.gif'

    image0 = images[0]
    image_end = images[-1]
    for i in range( 5 ):
        images.insert(0,image0)
    for i in range( 5 ):
        images.append(image_end)

    images[0].save( savepath, save_all=True, append_images=images[1:], optimize=not False, duration=100, loop=0 )
    print(' Exported : [%s]'%savepath)

NI_MAX = 10000
images=[]



# Initialize pygame
pygame.init()

# Open a display
srf = pygame.display.set_mode( (W,H) )

#

# Create a world object
world = ode.World()
world.setGravity((0,-9.81,0))

#

# Create two bodies
body0 = ode.Body(world)
M = ode.Mass()
M.setSphere(2500, 0.05)
body0.setMass(M)
body0.setPosition( (0,0,0) )

body1 = ode.Body(world)
M = ode.Mass()
M.setSphere(2500, 0.05)
body1.setMass(M)
body1.setPosition( (1,1,0) )

body2 = ode.Body(world)
M = ode.Mass()
M.setSphere(2500, 0.05)
body2.setMass(M)
body2.setPosition( (0,2,0) )

bodys = [body0,body1,body2]


# Connect body0 with the static environment
if True:
    j0 = ode.BallJoint(world)
    j0.attach(body1, ode.environment)
    j0.setAnchor( body0.getPosition() )
if False:
    j0 = ode.HingeJoint(world)
    j0.attach(body1, ode.environment)
    j0.setAnchor( body0.getPosition() )
    j0.setAxis( (0,0,1) )

    #j0.setParam(ode.ParamVel, 3)
    #j0.setParam(ode.ParamFMax, 22)

# Connect body1 with body0
j01 = ode.BallJoint(world)
j01.attach(body0, body1)
j01.setAnchor( body0.getPosition() )

# Connect body2 with body1
j12 = ode.BallJoint(world)
j12.attach(body1, body2)
j12.setAnchor( body1.getPosition() )

joints = [j0, j01, j12]




RGBs = { body0:(63, 63, 63), body1:(127, 63, 63), body2:(63, 127, 63) }


####################################################
# Simulation loop...

fps = 50
dt = 1.0/fps
loopFlag = True
clk = pygame.time.Clock()

ti = -1
while loopFlag:
    ti += 1
    events = pygame.event.get()
    for e in events:
        if e.type==QUIT:
            loopFlag=False

        if e.type==KEYDOWN:
            gif( images )
            loopFlag=False

    # Clear the screen
    srf.fill((255,255,255))


    for joint in joints:
        if joint is j0:
            continue
        rgb = (127,127,127)
        
        SIMPLE=False
        if SIMPLE:
            p0 = coord( joint.getBody(0).getPosition() )
            p1 = coord( joint.getBody(1).getPosition() )
        else:    ####        joint.attach(body, ode.environment)の場合のアンカ―位置への対応と、線の色分け
            b_move = None
            b0 = joint.getBody(0)
            if b0:
                if b0.getPosition() != joint.getAnchor():
                    b_move = b0
            if None is b0:
                p0 = coord( joint.getAnchor() )
                if None is p0:
                    p0 = coord( body0.getPosition() )
            else:
                p0 = coord( b0.getPosition() )
            b1 = joint.getBody(1)
            if b1:
                if b1.getPosition() != joint.getAnchor():
                    b_move = b1
            if None is b1:
                p1 = coord( joint.getAnchor() )
                if None is p1:
                    p1 = coord( body0.getPosition() )
            else:
                p1 = coord( b1.getPosition() )
        
            rgb = RGBs[b_move]

        lw = 5
        pygame.draw.line( srf, rgb, p0,p1, lw)

    for body in bodys:
        xyz = body.getPosition()
        rgb = RGBs[body]
        pygame.draw.circle(srf, rgb, coord( xyz ), 20, 0)
    
    pygame.display.flip()

    if ti % 4 == 0:
        storeImage(srf,images)

    # Next simulation step
    world.step(dt)

    # Try to keep the specified framerate    
    clk.tick(fps)
    
    #if not loopFlag:
    #    gif( images )
```

※　GIF画像の最大フレーム数は、画像ストア数上限 ```NI_MAX`` に依存。