例題16-4:やっぱ3Dでしょ

A: さて、VisionEggの刺激の中でまだ解説していないものを紹介するという今回の例題もそろそろ終わりが近づいてきた。今回は3Dの刺激を描画するモジュールを紹介する。

B: おお、3D。3Dといえば3こないだ帰省したら実家のテレビが3Dになっていましたよ。最初は面白がって見ていたんですが、なんだかそのうち頭が痛くなってきて…。

A: ふむ。アレは合わない人がいるだろうなあ。B君はあれがなんで立体的に見えるかわかってるか?

B: へへん。両眼視差ですよね。メガネが液晶シャッターになっていて、高速に左右のシャッターをON/OFFして、それに合わせて画面も左目用と右目用の…

A: ほう。B君が見たのはシャッター式の奴か。まあわかっているみたいで何より。

B: あんなのもPythonで簡単にできるんですねえ。すごいなあ。

A: いや、多分そんなに難しくないと思うが、今回言ってる3Dはそういうのじゃない。

B: へ? 違うんですか?

A: そうじゃなくてだな、ゲームとかでよくポリゴンを使ってキャラクターを表示してるのあるだろ。そういう3次元のデータを画面に表示する方法だ。

B: ああ。なるほど。それはそれで楽しみですねえ。ゲームみたいな3Dのキャラクターを表示できるのかあ。すごいなあ。

A: いやいや、それも違う。

B: えぇ? じゃあ何なんですか?!

A: 今回紹介するのは、VisionEggで用意されているクラスで、3Dの筒や球、板にテクスチャを貼って表示する方法だ。 ゲームみたいな複雑な3Dモデルを表示するとかいう話じゃない。

B: なーんだ。

A: さらに言うと、全部紹介するのは面倒くさいんで、今回は筒を表示するVisionEgg.Textures.SpinningDrumだけを紹介する。 代わりと言ってはなんだが、カメラの設定で注意すべき点があるのできちんと説明しておきたい。

B: カメラの設定? なんですかそれ。

A: まあ詳しい話は後にして、まずはサンプルを見てもらおうか。テクスチャのファイルも必要だから一緒にダウンロードしてね。

  • 行番号なしのソースファイルをダウンロード→ 16-6.py

  • ソースファイルと同じディレクトリに置いて使用→ 16-4.png

 1#!/usr/bin/env python
 2"""A texture-mapped spinning drum."""
 3
 4from VisionEgg import *
 5start_default_logging(); watch_exceptions()
 6
 7from VisionEgg.Core import *
 8from VisionEgg.Textures import *
 9
10import pygame
11import pygame.locals
12
13texture = Texture('16-4.png')
14
15screen = get_default_screen()
16projection = SimplePerspectiveProjection(fov_x=90.0)
17stimulus = SpinningDrum(texture=texture,shrink_texture_ok=1,position=(0,-0.1,-12),height=-1)
18viewport = Viewport(screen=screen,
19                    size=screen.size,
20                    projection=projection,
21                    stimuli=[stimulus])
22
23st = VisionEgg.time_func()
24waitkeypress = True
25while waitkeypress:
26    ct = VisionEgg.time_func()
27    for e in pygame.event.get():
28        if e.type == pygame.locals.KEYDOWN:
29            if e.key == pygame.locals.K_ESCAPE:
30                waitkeypress = False
31    
32    stimulus.parameters.angular_position = 180*(ct-st)
33    screen.clear()
34    viewport.draw()
35    swap_buffers()
36
37screen.close()

B: いつものAさんのプログラムとちょっと違う感じですね。最初の2行とか、from ほげほげ import ふがふが を使ってたりとか。

A: うん。VisionEggの公式サンプルプログラムをぱぱっと書き換えただけだからな。ポイントは16から21行目。あとは32行目かな。 まずは17行目のVisionEgg.Textures.SpinningDrum()。テクスチャを貼った筒を作る。注意すべき点としては、まずpositionの指定が3次元になっていること。 筒の大きさはradiusとheightで指定するんだが、ここではradiusは指定せずにデフォルト値の1.0を使っている。 heightの-1ってのは、負の数を指定すると貼り付けたテクスチャのサイズにぴったり合うように高さを計算してくれるんだ。

B: へえ、それは便利。

A: あとは筒の向きを指定するdrum_center_azimuthとかdrum_center_elevationってのがあるけど、私は使ったことないのでよくわからん。

B: 使ったことないって、向きを指定できなきゃ不便じゃないんですか?

A: 筒の向きは固定しておいて、カメラの向きを変えた方がわたしにはわかりやすいがな。

B: ???

A: まあそれは後程。あとshrink_texture_okってのも公式サンプルで指定されていたからつけているけど、正直よくわからん。

B: 最近解説のいい加減度がどんどんひどくなっているなあ。

A: helpを見るとAllow automatic shrinking of texture if too big? (Boolean) Default: Falseと書いてある。でかいテクスチャを貼った時に思い通りいかなかったらいじってみたらいいんではないかな。私は困ったことはない。 ていうかこんな刺激使いどころがよくわからない。

B: うわ、ひどい言いざま。

A: 今回のサンプルで紹介しておきたいのはSpinningDrumよりも透視投影の設定だ。18行目からのViewportの宣言で、今までは出てこなかったprojectionという引数が指定されている。B君、projectionとはどういう意味だ?

B: うへ? ええと、「プロジェクター」と同じですよね。プロジェクターは日本語で…なんて言うんだろ。

A: projectionは「投影」だ。今B君がプロジェクターって言ったけど、プロジェクターがスクリーンに画面を映すように、VisionEgg.Core.Viewport()は刺激をスクリーンに「投影」する機能を持つ。いや、正確に言うとプロジェクターのように「映し出す」んではなくてカメラのように3次元の物体を2次元のスクリーンに「撮影する」といった方が正しいかな。 例題1-4 の図でVisionEgg.Core.Viewportをカメラっぽいアイコンで示しているのはその辺を表現したかったからだったりする。

B: 例題1-4とはこれまた懐かしい。

A: で、VisionEggで使える投影には大きく分けて2種類ある。正投影(正射影)と透視投影だ。それぞれVisioEgg.Core.OrthographicProjectionとVisionEgg.Core.PerspectiveProjectionというクラスで実現されている。

B: ん? 美術の授業で聞いたことがあるような気がするぞ。正投影っていうのは確か…

A: ほうほう、確か?

B: …正しく投影、する?

A: なんだそりゃ。じゃあ透視投影はなんなんだよ。

B: 後ろが透けて見える?

A: …この図を見てもらおうか。

../_images/16-4-01.png

B: ええと、スクリーンに垂直に投影するのが正投影。一点に収束するように投影するのが透視投影。

A: 正投影にはいろんな種類があるんでそう言い切っちゃうとどうかと思わなくもないけど、まあだいたいそんなもんかな。

B: 具体的にどんなふうに違うんですかね。

A: どれどれ、ぱぱっと作ってみるか…。そりゃ、どうだ。

../_images/16-4-02.png

B: んんー。正投影はなんだか変な感じですねえ。直方体が歪んで見える。っていうか、これ、直方体なんですよね? もともと歪んでるってオチは…。

A: 直方体だよ。正投影で描かれている図なんて結構世の中にありふれてるんだが、とりたてて変に感じないだろ?

B: うーん、そう言われてみれば以前の例題に出てきた図も正投影で描かれてるやつが結構ありますね。

A: さて、正投影は使いようによっては面白い表現が出来るが、心理実験でわざわざ3Dのオブジェクトを表示しようって場合、たいてい透視投影の方が目的に沿っているだろう。てなわけで透視投影だが、VisionEgg.Core.PerspectiveProjectionでは投影するスクリーンのtop、bottom、left、right、それにnearとfarを指定する。

B: ちょ、なんのことかさっぱりわかりません。スクリーンのtopとかbottomってのはわかりますが、nearとかfarって何ですか?

A: あまりにもスクリーンに近すぎるオブジェクトや、遠すぎるオブジェクトはレンダリングしない方がよいことがある。nearとfarはレンダリングする範囲の手前と奥を指定するためのパラメータだ。

B: あの、レンダリングってのは…

A: この場合は、物体の形状データやカメラの位置から画像の個々のピクセルの値を計算することだな。要するにnearより近いオブジェクトと、farより遠いオブジェクトは画面に描画されないんだよ。下のような例だと、赤と青の直方体は描画されない。

../_images/16-4-03.png

B: なるほど。

A: んで、撮影範囲をtopとかbottomとかで指定するんじゃなくて、画角で指定することも出来る。それがこのサンプルでも使っているVisionEgg.Core.SimplePerspectiveProjection。16行目だね。 引数fov_xは水平方向の画角を指定する。垂直方向はaspect_ratioで水平方向に対して相対的に指定する。

B: アスペクト比ってディスプレイ買うときにカタログで見ましたよ。ピクセルの縦横の比ですよね。

A: んむ。まあこういうのは見てもらうのが早い。以下の画像はどれもオブジェクトやカメラ位置は固定したままで、fov_xとaspect_ratioのみを変更している。

../_images/16-4-04.png

B: fov_xが小さいと望遠で、大きいと広角ですね。ふむふむ。

A: あと、nearとfarはそれぞれz_clip_near、z_clip_farという風になぜか引数の名前が変わっているので注意。さて、これで投影の設定ができた。20行目のようにVisionEgg.Core.SimplePerspectiveProjectionのインスタンスをVisionEgg.Core.Viewportの引数projectionに指定してやれば、このViewportは透視投影で刺激を描画する。実行するとこんな感じだ。

../_images/16-4-05.png

B: おお、これは散髪屋のくるくるが上に登って行くように見える錯視ですね。

A: この「くるくる」はサインポールと言うらしいがな。ちなみに回転しているのは、32行目でVisionEgg.Textures.SpinningDrumのangular_positionというパラメータを更新しているからだ。SpinningDrumという名前だからといって勝手に回転するわけじゃないので注意。

B: あとは特に難しいところはないですね。

A: さて、16-6.pyはこの位にしておいて、次のサンプルへ行こうか。さっき「物体を固定しておいてカメラを動かした方が簡単」といったけど、そのカメラを動かす方法の話をする。

B: あのー。さっき聞き逃したんですけど、なんでカメラを動かす方が簡単なんですかね。

A: 例えば大量の面を持つ3Dモデルを運動させようとするとだな、1コマ毎にオブジェクトが新しい位置へ移動した時に、全ての面がどの位置へ移動したかすべて計算せにゃならん。やってみりゃわかるが、面数によっては最近のPCに積まれているCPUやGPUでも間に合わない重い重い計算だ。一方、物体を固定しておいてカメラを動かしたら、全く動かしていない時と同様に1回レンダリングをすればいいだけだ。物体を移動させてもどうせ1回レンダリングしなきゃならんのだから、カメラを動かした方がそりゃもう断然楽だ。あたり前田のクラッカーってやつだ。

B: クラッカー? なんですかそれ。

A: そこは聞き流せよ。その前の部分はわかったのか?

B: よくわかりません。

A: …。まあいい。とにかく、レンダリングの時はカメラの内在的(intrinsic)なパラメータと外在的(extrinsic)なパラメータを特定しないといけない。外在的なパラメータはVisionEgg.Core.ModelViewのインスタンスによって指定される。

B: ??? 外在的?

A: 簡単に言うと、カメラをどの位置で、どっち向きに構えるかだ。内在的なパラメータってのはさっきのfov_xとかaspect_ratioとかにあたる。レンズの焦点距離とかはそれぞれのカメラに固有のものだからintrinsic、それに対してカメラをどの位置からどう構えるかってのはそれぞれのカメラに固有のものではないからextrinsicというわけだ。

B: カメラによってどう構えられるかは変わってくると思いますが…。TV用のでっかいカメラとかを縦に構えたりとか出来ないと思いますし。

A: しょーもないヘリクツを言うな。とにかく、このVisionEgg.Core.ModelViewなんだが、helpを見てもロクな説明がなくて厄介なんだ。が、直接こいつを操作しなくても、VisionEgg.Core.SimplePerspectiveProjectionのメソッドから操作できるようになっている。具体的にはサンプルプログラム16-7.pyの68行目に出てくるlook_at()というメソッドである。

B: AさんAさん、まだ16-7.pyのソースを見せてもらってませんが。それにしてもなんでいきなりである調。

A: おお、忘れてた。これが16-7.pyのソースだ。

  • 行番号なしのソースファイルをダウンロード→ 16-7.py

  • ソースファイルと同じディレクトリに置いて使用(16-6.pyのものと同じ)→ 16-4.png

 1#!/usr/bin/env python
 2"""A texture-mapped spinning drum."""
 3
 4from VisionEgg import *
 5start_default_logging(); watch_exceptions()
 6
 7import VisionEgg.Core
 8import VisionEgg.Textures
 9import VisionEgg.WrappedText
10import math
11
12import pygame
13import pygame.locals
14
15texture = VisionEgg.Textures.Texture('16-4.png')
16
17screen = VisionEgg.Core.get_default_screen()
18projection = VisionEgg.Core.SimplePerspectiveProjection(fov_x=90.0)
19pmat_original = projection.get_matrix()
20stimulus = VisionEgg.Textures.SpinningDrum(texture=texture,
21                                           shrink_texture_ok=1,
22                                           position=(0,0,0),
23                                           height=-1)
24viewport = VisionEgg.Core.Viewport(screen=screen,
25                                   projection=projection,
26                                   stimuli=[stimulus])
27
28msgtext = '''UP:    Zoom In
29DOWN:  Zoom Out
30LEFT:  Rotate Counter-Clockwise
31RIGHT: Rotate Clockwise
32'''
33
34msg = VisionEgg.WrappedText.WrappedText(text=msgtext,
35                                        position=(50,150))
36
37instview =VisionEgg.Core.Viewport(screen=screen, stimuli=[msg])
38
39eyepos = [10.0,1.0,0.0]
40top = [0.0,1.0,0.0]
41tilt = 90.0
42
43st = time.clock()
44waitkeypress = True
45while waitkeypress:
46    ct = time.clock()
47    stimulus.parameters.angular_position = 180*(ct-st)
48    
49    for e in pygame.event.get():
50        if e.type == pygame.locals.KEYDOWN and e.key == pygame.locals.K_ESCAPE:
51            waitkeypress = False
52            
53    k = pygame.key.get_pressed()
54    if k[pygame.locals.K_UP]:
55        if eyepos[0]>0.5:
56            eyepos[0] -= 0.5
57    elif k[pygame.locals.K_DOWN]:
58        eyepos[0] += 0.5
59    elif k[pygame.locals.K_RIGHT]:
60        tilt -= 5
61    elif k[pygame.locals.K_LEFT]:
62        tilt += 5
63    
64    top[2] = math.cos(tilt*math.pi/180.0)
65    top[1] = math.sin(tilt*math.pi/180.0)
66    
67    projection.parameters.matrix = pmat_original
68    projection.look_at(eyepos,(0,0,0),top)
69    
70    screen.clear()
71    viewport.draw()
72    instview.draw()
73    VisionEgg.Core.swap_buffers()
74
75screen.close()

B: …16-6.pyベースみたいですが、結構変わってますね。むむう。

A: カメラの移動に関する変更点は2か所だけだ。67行のlook_at()メソッドは3個の引数を取る。最初はカメラの位置を指定する3次元のベクトル。2番目はカメラが向ける位置を指定する3次元のベクトル。最後はカメラの上がどちらを向いているかを示すこれまた3次元のベクトルだ。

B: カメラの位置と向ける位置ってのはわかりますが、最後のパラメータがよくわかりませんね。

A: うん。私にもよくわからん。

B: へっ? わかってないのにプログラム書いてんですか?

A: この辺の描画はPyOpenGLに基づいてるはずなんで、OpenGLをきちんと勉強すりゃわかると思うんだがな。正直私程度の使い方だと適当にぱぱっと指定して、思い通りにならなきゃ書き直してってやってりゃ充分なんでな。

B: 例題16は全体的に解説ひどくないですか?

A: いや、こんな感じになってしまうのは目に見えてたんで扱ってこなかったんだがな。逆に言うとこの程度の理解でも使えちゃうってことなんだよな。そう思ったら開き直っちゃって。

B: なんだか悪い見本にしかなっていない気がする…。

A: 最初の方の例題にも書いたけど、きちんと理解したい人は自分で補ってほしい。他人にきちんと説明できるレベルを目指すと本業をする時間がなくなっちゃうよ。ホントに。…ただの面倒くさがりじゃないぞ?

B: まあそういうことにしておいてあげましょう。

A: このサンプルプログラムでは、キーボードのカーソルキーでサインポールに近づいたり離れたり、カメラを視軸で回転したりできるようにしている。カーソルキーを押しっぱなしにしてくるくる回せるように、いつものpygame.event.get()じゃなくてpygame.key.get_pressed()を使ってキーの状態を取得している。忘れた人は 例題12-4 を参考にしてほしい。

B: ESCキーだけはpygame.event.get()で検出してるんですね。変なの。

A: pygame.key.get_pressed()だと、実行したその瞬間にキーが押されていなければ検出されない。だから、ESCを軽くポンと押しただけだと検出できない場合がある。こういう用途にはpygame.event.get()の方がいい。

B: ふうん。相変わらず何も考えてないようでちょっとは考えているんですねえ。

A: ちょっとは余計だ。それよりも気を付けてほしいのは、19行目と67行目だ。look_at()メソッドをwhileループの中で何度も呼び出すと、どんどんカメラがあらぬ方向へすっ飛んでいってしまう。VisionEggのソースをちゃんと読んでないので推測なんだが、look_at()メソッドは破壊的な動作をするらしい。

B: 破壊的?!

A: 要するに、カメラ行列だか何だかを書き換えてしまうということだ。書き換えられてしまった行列に対して変換を施すので、どんどん意図しない方向へ運動していってしまう。

B: …(何言ってんだかさっぱりわからない)

A: とにかく、最初にVisionEgg.Core.SimplePerspectiveProjectionのインスタンスを作成した時の行列を保存しておいて、look_at()を呼び出す前に復元してやれば意図したとおりに働く。それをやってるのが19行目と67行目で、まず18行目でVisionEgg.Core.SimplePerspectiveProjectionのインスタンスを作成した直後の19行目で、get_matrix()メソッドを使って行列を保存しておく。そして68行目でlook_at()する直前の67行目で、データ属性parameters.matrixに先ほどのget_matrix()の戻り値を格納する。これでインスタンス作成直後の行列が復元されるので、安心してlook_at()出来る。

B: 何を言ってるのかさっぱりわかりませんが、要するにこの手順を踏めばいいんですね?

A: 他にスマートな方法があるのかもしれないが、とりあえずこうすれば動く。それで充分だろ。

B: はあ。まあ。

A: 出来れば、一度は67行目をコメントアウトして、この行列を復元するという処理をしなければ何が起こるのか確認しておいてほしい。たぶん一瞬サインポールが見えるだけであっという間に灰色一色の画面になってしまうはず。

B: わざわざうまくいかない事がわかっているのを試さなくても…

A: いやいや、どう失敗するのかを知った方が理解が深まるという面もある。さて、これで解説しておきたかったことは大体終わったが、最後にVisionEggの便利なモジュールを紹介しておこう。VisionEgg.WrappedTextだ。

B: Wrappedって、何かに包まれてるんですか?

A: いや、そうじゃなくて。VisionEgg.Text.Textでは複数行にわたるテキストを表示することが出来ない。複数行のテキストを表示できるのがVisionEgg.WrappedText.WrappedTextなんだよ。

B: 毎度のことながらText.TextとかWrappedText.WrappedTextとか、同じ綴りが続くのは何とかならないのかなあ。

A: んー。私も最初はそう思ってたけど、自分でパッケージを作ってみるとこの辺りの難しさはわかるような気がする。まあWrappedTextはVisionEgg.Textに入れても良かったような気がしないでもないが。とにかくサンプルでは9行目でimportして28行目から35行目でインスタンスを生成している。今までのVisionEgg.Text.Textと違って引数textに渡す文字列内に改行文字(\n)を含むことが出来る。あとサンプルでは指定していないがsizeという引数が用意されている。横幅、縦幅をタプルで指定すればその中に文字列を折りたたんで収まるように努力してくれる。

B: ど、努力ですか?

A: うむ。ちなみにsizeを指定しなければスクリーンの幅と高さが設定される。で、71行目から72行目はちょっとした小技だが、Viewportはswap_buffers()する前に複数個呼び出すことによって重ね描きすることが出来る。まあそれ自体はすでに 例題16-1 でも見たことだが、それぞれのViewportで別々のProjectionを使うことが出来る。だから、サインポールは透視投影で描いておいて、文字列はスクリーン上の位置を計算しやすい通常の投影で描くということをやっている。

B: ふむふむ。便利ですな。

A: 解説はこんなもんかな。んじゃ、実行してみるぞ。

../_images/16-4-06.png

B: 左下にテキストが表示されていますね。これ、動かしてみてもいいですか?

A: どうぞ。

B: よいしょっと。おお、ぐいぐい動くぞ。…っと、ズームインしたら円筒の内側に突き抜けちゃいましたが。

../_images/16-4-07.png

A: 円筒の半径より内側まで近寄れるように作ってあるからな。

B: これ、円筒の内側にもテクスチャが貼られてるんですねえ。これは使いようによっては面白い画が作れるかも。

A: VisionEggの公式サンプルは円筒の内側にカメラを置いてるので、興味があれば公式サンプルも見ておいてほしい。さて、これで一応投影の話もしたし、あとは動画再生の話をしたら例題16は終了かな。ではでは。