Panda3D 3次元ゲームエンジン入門
【概要】Panda3Dはオープンソースの3次元ゲームエンジンである。本記事では、Pythonプログラムから3次元空間を生成し、モデルを配置し、照明を当て、キーボードやマウスで操作する流れを、10の演習を通して学ぶ。
【前提知識】Pythonの基本文法(クラス、継承、メソッド定義)。
【学修のねらい】3次元コンピュータグラフィックスの基本要素(座標系、視点、モデル、変換、照明、イベント、アニメーション、運動)を、演習を通して理解する。
【目次】
- はじめに
- 本教材を読む前に:3つの基本概念
- 前準備
- 演習1: 画面を開く
- 演習2: マウスによる視点操作
- 演習3: 複数の3次元モデルの読み込み
- 演習4: 3次元モデルの配置
- 演習5: 照明を当てる
- 演習6: キーボードへの反応
- 演習7: マウスクリックへの反応
- 演習8: 矢印キーで立方体を動かす
- 演習9: 立方体を自動で動かす
- 演習10: 速度を持たせて動かす
【サイト内の関連ページ】
はじめに
Panda3Dプログラムは、次の4ステップで構成される。本記事の演習はすべてこの構成に沿う。
┌─────────────────────────────────────┐
│ 1. モジュールのインポート │
│ from direct.showbase.ShowBase │
│ import ShowBase │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 2. アプリケーションクラスの定義 │
│ class MyApp(ShowBase) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 3. シーンの構築 │
│ モデルの読み込み・配置・照明・ │
│ イベント登録など │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 4. アプリケーションの実行 │
│ app.run() │
└─────────────────────────────────────┘
ステップ3で行う内容は演習ごとに異なる。演習1ではシーンに何も構築しないが、演習が進むにつれてモデルの読み込み・配置・照明・イベント登録などを追加していく。
本教材を読む前に:3つの基本概念
演習に繰り返し登場する3つの基本概念を最初にまとめる。各演習ではこれらを既知のものとして扱う。コードの中で疑問が生じたらこの節に戻って確認するとよい。
概念1:シーングラフ(ノードの木構造)
Panda3Dのシーンは、ノードと呼ばれる要素が親子関係でつながった木構造として管理される。これをシーングラフと呼ぶ。木の根(最上位)にあたるのがrenderという特別なノードである。
render(根:シーン全体の最上位ノード)
├─ environment(竹林のモデル)
├─ rgbCube(立方体のモデル)
└─ ライト など
モデルを読み込んだだけでは、そのモデルはまだ木のどこにも属しておらず、画面に表示されない。reparentTo(self.render)は「このモデルをrenderの子として取り付ける」操作である。画面に表示されるのは、renderの子孫であるノードだけである。「親に取り付ける」操作が随所に出てくるのは、このためである。なお、シーングラフ上のノードを指す変数には、慣習として末尾にNp(NodePath、ノードへの参照を表す型の略)を付けることがある。
概念2:importなしで使えるグローバル変数
Panda3Dは、ShowBaseを起動すると使えるようになるグローバル変数を提供している。これらはimport文や自分での定義なしに利用できる。本教材で使うものは次の通りである。
self.loader:モデルを読み込む。self.render:シーングラフの根(概念1)。self.taskMgr:毎フレーム呼ばれる関数(タスク)を管理する(演習9以降で使用)。globalClock:時間を管理する。globalClock.getDt()で前フレームから今フレームまでの経過秒数を取得できる(演習10で使用)。
globalClockはself.を付けずに使うグローバル変数である。コード中に定義が見当たらなくても誤りではない。
概念3:フレームとアニメーション
Panda3Dは1秒間に何回も画面を描き直している。その1回分を「フレーム」と呼ぶ。毎フレーム、モデルの状態(位置など)を少しずつ更新し続けると、人の目にはなめらかに動いて見える。これがアニメーションの基本原理である。
1フレームにかかる時間は一定とは限らない。パソコンの性能や処理の重さによって、フレームの間隔は変動する。そこで動かす量を計算するときは、移動の速さに前フレームから今フレームまでの経過秒数(dt)を掛ける。フレームが速く描かれるとき(dtが小さい)は1回あたりの移動量を小さく、遅いとき(dtが大きい)は大きくすることで、実時間あたりの動く速さが一定に保たれる。この考え方は演習10で使う。
前準備
Pythonのインストール
本教材はPythonが動作する環境を前提とする。Pythonが未導入の場合は事前にインストールする。
Panda3Dのインストール
本記事の演習はPanda3Dを使う。Panda3Dは3次元コンピュータグラフィックスのためのライブラリである。インストールは1回だけ行う。
コマンドプロンプトを管理者権限で起動する(Windowsキー → 「cmd」と入力 → 右クリック → 「管理者として実行」)。起動したコマンドプロンプトに次を入力して実行する。
pip install panda3d
プログラムの実行
各演習のコードを実行する方法は2通りある。
方法A:Visual Studio Codeで実行する。Visual Studio Codeがインストール・設定済みであれば、本ページのコードを編集画面に貼り付け、実行する。
方法B:ファイルに保存して実行する。テキストエディタを開き、本ページのコードを貼り付ける。a.pyのようなファイル名で保存する。コマンドプロンプトでそのファイルがあるフォルダに移動し、次を入力して実行する。
python a.py
以下、各演習では「コードを実行する」と記述する。方法AまたはBで実行する。プログラムを終了するときは、表示されたウィンドウ右上の「×」をクリックする。
演習1: 画面を開く
最小構成のプログラムで画面を表示し、Panda3Dが動作することを確認する。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) app = HelloWorld() app.run()
- 何も描かれていない灰色のウィンドウが表示されることを確認する。
《ヒント》
- 灰色のウィンドウが表示されれば、Panda3Dは正しくインストールされている。
- エラーが出る場合は、PythonとPanda3Dの導入を見直す。
《考察ポイント》
- この時点で3次元空間はすでに用意されているが、そこに何も配置されていないため灰色に見える。次の演習からは、この空間にモデルを配置する。
演習2: マウスによる視点操作
3次元モデルを1つ読み込んで表示し、マウスで視点を動かす。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) app = HelloWorld() app.run()
補足:
loadModel()はモデルファイルを読み込む。reparentTo(self.render)は、読み込んだモデルをrenderの子として取り付ける(概念1)。Panda3Dには標準モデルが同梱されており、"models/environment"のような相対指定で読み込める。 - 竹林のような3次元シーンが表示されることを確認する。
- マウスで視点を動かす。
- 左ボタンを押しながらドラッグ:視点の向きを変える(上下左右)。
- 右ボタンを押しながらドラッグ:視点を前後に動かす(ズームに相当)。
《ヒント》
- 同じシーンを横、後ろ、上など複数の角度から観察する。
- 視点を遠ざけると、地面の起伏や木々の奥行きの見え方が変わる。
《考察ポイント》
- マウスで動いているのは視点であり、シーンそのものは動いていない。3次元コンピュータグラフィックスでは「シーン」と「視点」が分離している。
- 画面は2次元であるが、立体に見える。立体感の手がかりを観察する(隠れる順序、大きさの変化、遠近)。影については演習5で扱う。
演習3: 複数の3次元モデルの読み込み
2つのモデルを同じシーンに読み込む。
Panda3Dには複数の標準モデルが同梱されており、いずれも"models/..."という相対指定で読み込める。本演習で使う2つのモデルは次の通りである。
"models/environment":環境(竹林)のモデル。"models/misc/rgbCube":6面の色が異なる立方体。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) app = HelloWorld() app.run()
- 竹林のシーンの中に立方体が表示されることを確認する。マウスで視点を変えて、立方体を複数の角度から観察する。
《ヒント》
- 立方体は6面それぞれに色がついている。視点を回しながら、見える面が切り替わる様子を観察する。
- 追加演習:
"models/misc/rgbCube"を"models/misc/sphere"に置き換えて実行すると、球が表示される。self.cube = self.loader.loadModel("models/misc/sphere") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0)
《考察ポイント》
- 1つのシーンに複数のモデルを配置できる。シーンとは「3次元空間に配置されたモデルの集まり」である。
- 立方体と球では、視点を回したときの見え方が異なる(球はどの方向から見ても丸い)。形状そのものが立体感の表現に与える影響を観察する。
演習4: 3次元モデルの配置
モデルの位置・大きさ・向きを制御する。3次元コンピュータグラフィックスの基本変換である移動・スケール・回転を順に実施する。
演習4-1: 位置を指定して配置
Panda3Dの座標系:xは左右(右が+)、yは前奥(カメラから見て奥が+)、zは上下(上が+)。z-up(zを上方向とする)の右手座標系である。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube1 = self.loader.loadModel("models/misc/rgbCube") self.cube1.reparentTo(self.render) self.cube1.setPos(-3, 20, 0) self.cube2 = self.loader.loadModel("models/misc/rgbCube") self.cube2.reparentTo(self.render) self.cube2.setPos(3, 20, 0) app = HelloWorld() app.run()
- 2つの立方体が画面の左右に離れて表示されることを確認する。マウスで視点を動かして位置関係を確かめる。
《ヒント》
setPosの3つの数値を(-3, 20, 0)から(0, 20, 5)や(0, 50, 0)に変更し、立方体の位置を実行前に予想してから確かめる。- yの値を大きくすると、立方体は遠ざかり、小さく見える。
《考察ポイント》
- 2次元では座標は(x, y)で済むが、3次元では(x, y, z)の3つが必要になる。「奥行き」という軸が増えることで何が表現できるかを考える。
- 画面上で「小さく見える」ことと、実際に「小さく作られている」ことは別である。次の演習で確認する。
演習4-2: setScale()で拡大縮小
setScale(値)でモデルそのものの大きさを変える。1.0が基準(等倍)である。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube1 = self.loader.loadModel("models/misc/rgbCube") self.cube1.reparentTo(self.render) self.cube1.setPos(0, 20, 0) self.cube2 = self.loader.loadModel("models/misc/rgbCube") self.cube2.reparentTo(self.render) self.cube2.setPos(5, 20, 0) self.cube2.setScale(2.4) app = HelloWorld() app.run()
- 右側の立方体が2.4倍に拡大されていることを確認する。
《ヒント》
- 追加演習:倍率を変えた5つの立方体を一列に配置する。サイズが段階的に変化する様子を観察する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube1 = self.loader.loadModel("models/misc/rgbCube") self.cube1.reparentTo(self.render) self.cube1.setPos(-6, 20, 0) self.cube1.setScale(0.2) self.cube2 = self.loader.loadModel("models/misc/rgbCube") self.cube2.reparentTo(self.render) self.cube2.setPos(-3, 20, 0) self.cube2.setScale(0.6) self.cube3 = self.loader.loadModel("models/misc/rgbCube") self.cube3.reparentTo(self.render) self.cube3.setPos(0, 20, 0) self.cube3.setScale(1.0) self.cube4 = self.loader.loadModel("models/misc/rgbCube") self.cube4.reparentTo(self.render) self.cube4.setPos(3, 20, 0) self.cube4.setScale(1.4) self.cube5 = self.loader.loadModel("models/misc/rgbCube") self.cube5.reparentTo(self.render) self.cube5.setPos(6, 20, 0) self.cube5.setScale(1.8) app = HelloWorld() app.run()
《考察ポイント》
- 小さなモデルを近くに配置した場合と、大きなモデルを遠くに配置した場合では、画面上の見た目が似ることがある。画面に映ったサイズだけでは奥行きを判断できない。
- 奥行きを判断する手がかりを考える(他のモデルとの位置関係、地面との接点など)。
演習4-3: setHpr()で回転
setHpr(H, P, R)でモデルの向き(回転)を変える。H・P・Rはオイラー角と呼ばれ、単位は度である。飛行機の姿勢にたとえると次のようになる。
H(Heading):z軸(上下軸)まわりの回転。左右に向きを変える。P(Pitch):x軸(左右軸)まわりの回転。機首を上下させる。R(Roll):y軸(前後軸)まわりの回転。機体を左右に傾ける。
setHpr(45, 0, 0)は「z軸まわりに45度回す」という意味になる。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube1 = self.loader.loadModel("models/misc/rgbCube") self.cube1.reparentTo(self.render) self.cube1.setPos(0, 20, 0) self.cube2 = self.loader.loadModel("models/misc/rgbCube") self.cube2.reparentTo(self.render) self.cube2.setPos(5, 20, 0) self.cube2.setHpr(45, 0, 0) app = HelloWorld() app.run()
- 左側の立方体(回転なし)と、右側の立方体(z軸まわりに45度回転)を見比べる。
《ヒント》
- 追加演習:
setHprのH(第1引数)を0, 20, 40, 60, 80と段階的に変えた5つの立方体を配置する。角度の変化が向きの変化として現れる。from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube1 = self.loader.loadModel("models/misc/rgbCube") self.cube1.reparentTo(self.render) self.cube1.setPos(-6, 20, 0) self.cube1.setHpr(0, 0, 0) self.cube2 = self.loader.loadModel("models/misc/rgbCube") self.cube2.reparentTo(self.render) self.cube2.setPos(-3, 20, 0) self.cube2.setHpr(20, 0, 0) self.cube3 = self.loader.loadModel("models/misc/rgbCube") self.cube3.reparentTo(self.render) self.cube3.setPos(0, 20, 0) self.cube3.setHpr(40, 0, 0) self.cube4 = self.loader.loadModel("models/misc/rgbCube") self.cube4.reparentTo(self.render) self.cube4.setPos(3, 20, 0) self.cube4.setHpr(60, 0, 0) self.cube5 = self.loader.loadModel("models/misc/rgbCube") self.cube5.reparentTo(self.render) self.cube5.setPos(6, 20, 0) self.cube5.setHpr(80, 0, 0) app = HelloWorld() app.run()
- 第2引数(P)や第3引数(R)だけを変えると、別の軸まわりに回る。
setHpr(0, 45, 0)やsetHpr(0, 0, 45)を試し、どの軸で回るかを確かめる。
《考察ポイント》
- 回転させると、それまで見えなかった面が見えるようになる。立方体は6面とも色が異なるため、回転すると配色が変わって見える。これは2次元の図形では起きない現象である。
- 位置・大きさ・向きの3つを組み合わせると、3次元空間内のさまざまな配置を表現できる。これがコンピュータグラフィックスの基本変換である。
演習5: 照明を当てる
これまでの演習では、モデルは平坦に見えていた。Panda3Dでは初期状態で照明が当たっていないため、面の向きによる明暗が表現されない。本演習では照明を追加し、立体感を与える。
本演習で使う2種類の光:
AmbientLight(環境光):シーン全体を一様に照らす光。これだけでは明暗がつかないが、影の部分が真っ暗になるのを防ぐ。DirectionalLight(指向性光):太陽光のように一定方向から差す光。面の向きに応じて明暗がつき、立体感が生まれる。指向性光は位置ではなく「向き」で決まる。光源が空間のどこにあるかは関係なく、どの方向から平行に光が差すかだけが効く。この向きはsetHpr()で指定する。
光は次の3ステップで適用する。光のオブジェクトを作り、render.attachNewNode()でシーングラフに取り付け、render.setLight()でシーン全体に適用する。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase from panda3d.core import AmbientLight, DirectionalLight class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) self.cube.setHpr(45, 0, 0) ambient = AmbientLight("ambient") ambient.setColor((0.3, 0.3, 0.3, 1)) ambientNp = self.render.attachNewNode(ambient) self.render.setLight(ambientNp) directional = DirectionalLight("directional") directional.setColor((0.8, 0.8, 0.8, 1)) directionalNp = self.render.attachNewNode(directional) directionalNp.setHpr(-45, -60, 0) self.render.setLight(directionalNp) app = HelloWorld() app.run()補足:立方体に
setHpr(45, 0, 0)を付けているのは、複数の面が見えるよう傾け、照明による明暗を観察しやすくするためである(演習4-3を参照)。以降の演習でもこの目的で傾けて表示する。 - 立方体や竹林のシーンに明暗がつき、立体的に見えることを確認する。照明を入れる前(演習4-3)と見比べる。
《ヒント》
- 指向性光の
setHprの値を変えると、光の差す方向が変わり、明るい面と暗い面が入れ替わる。 - 環境光の色(明るさ)を
(0.0, 0.0, 0.0, 1)に近づけると、光の当たらない面が暗くなる。(0.6, 0.6, 0.6, 1)に近づけると、全体が明るく平坦になる。 setColorのRGBに偏りをつける(例:(0.8, 0.2, 0.2, 1))と、色のついた光になる。
《考察ポイント》
- 照明を入れる前と後で、同じモデルでも立体感が変わる。立体感は形だけでなく、面の向きと光の当たり方(明暗)によっても生まれる。
- 環境光だけでは平坦に見え、指向性光を加えると立体感が出る。2種類の光が異なる役割を担っている。
演習6: キーボードへの反応
ユーザーの入力に反応するシーンを作る。まずキーボード入力に対応させる。
self.accept(キー名, 関数)は、指定したキーが押されたときに、その関数を呼び出すよう登録する(イベントハンドラの登録)。
キー名の例:"a", "space", "enter", "escape", "arrow_left", "arrow_up", "arrow_down", "arrow_right"
本演習からは、"escape"キーでプログラムを終了できるようにする(self.userExitはアプリケーションを終了するメソッドである)。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) self.cube.setHpr(45, 0, 0) self.accept("a", self.a_key) self.accept("escape", self.userExit) def a_key(self): self.cube.setScale(2) app = HelloWorld() app.run()
- 立方体が表示されることを確認する。
- キーボードの「A」キーを押すと、立方体が拡大されることを確認する。
- 「Esc」キーを押すと、プログラムが終了することを確認する。
《ヒント》
- 「A」キーが反応しない場合は、Panda3Dのウィンドウをクリックしてフォーカスを移してからキーを押す(フォーカスとは、現在キー入力を受け取るウィンドウのこと)。
- 追加演習:スペースキーで5倍に拡大するように書き換えて実行する。
self.accept("space", self.space_key) def space_key(self): self.cube.setScale(5)
《考察ポイント》
- これまでのプログラムでは、シーンは起動時の状態のまま動かなかった。キー入力への反応を組み込むと、プログラムが待ち受け状態になり、入力に応じてシーンが変化する。これがゲームやシミュレーションの基本である。
演習7: マウスクリックへの反応
マウスクリックで立方体を変化させる。
マウスボタンの名前:"mouse1"(左)、"mouse2"(中)、"mouse3"(右)
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) self.cube.setHpr(45, 0, 0) self.accept("mouse1", self.mouse1) self.accept("mouse3", self.mouse3) self.accept("escape", self.userExit) def mouse1(self): self.cube.setScale(2) def mouse3(self): self.cube.setScale(0.5) app = HelloWorld() app.run()
- 立方体が表示されることを確認する。
- マウスの右ボタンをクリックすると、立方体が縮小されることを確認する。
- マウスの左ボタンをクリックすると、立方体が拡大されることを確認する。
《ヒント》
- 左右両方を複数回クリックして、サイズが切り替わる様子を確認する。
- 演習2の「左ドラッグで視点移動」と、本演習のボタンクリックは両立する(クリックとドラッグは別の操作として扱われる)。
《考察ポイント》
- キーボードでもマウスでも、書き方は同じパターン(入力の名前と関数を結びつける)である。同じパターンを使うことで、入力の種類が増えても対応できる。
演習8: 矢印キーで立方体を動かす
現在の位置を取り出し、値をずらして、書き戻す処理を繰り返すことで、立方体を移動させる。
getX(), getY(), getZ():現在の位置(各軸の値)を取り出す。setX(値), setY(値), setZ(値):位置(各軸の値)を書き換える。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) self.cube.setHpr(45, 0, 0) self.accept("arrow_up", self.up_key) self.accept("arrow_down", self.down_key) self.accept("arrow_right", self.right_key) self.accept("arrow_left", self.left_key) self.accept("escape", self.userExit) def up_key(self): self.cube.setZ(self.cube.getZ() + 1) def down_key(self): self.cube.setZ(self.cube.getZ() - 1) def right_key(self): self.cube.setX(self.cube.getX() + 1) def left_key(self): self.cube.setX(self.cube.getX() - 1) app = HelloWorld() app.run()
- 立方体が表示されることを確認する。
- 矢印キー(上、下、右、左)を押すと、立方体が1単位ずつ移動することを確認する。
《ヒント》
- 上下キーはz(上下方向)、左右キーはx(左右方向)を変化させる。
- 上キーと右キーを交互に押して、立方体を斜めに移動させる。
《考察ポイント》
- キーを1回押すごとに立方体が1単位ずつ進む。なめらかには動かない。これは、キーが押された瞬間にのみ位置を更新しているためである。次の演習では、毎フレーム位置を更新する方法を学ぶ。
演習9: 立方体を自動で動かす
キー入力を待たずに、毎フレーム自動で位置を更新する。
self.taskMgr.add(関数, 名前)で関数を登録すると、その関数が毎フレーム自動的に呼び出される(このような関数を「タスク」と呼ぶ)。タスク関数の引数taskには現在のタスクの状態が入っており、task.timeはタスクが最初に実行されてからの経過秒数である(本演習では起動直後にタスクを登録するため、アプリ起動からの経過秒数とみなしてよい)。task.contは「次のフレームも継続して呼ぶ」ことを示す戻り値である(フレームについては概念3を参照)。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) self.cube.setHpr(45, 0, 0) self.accept("escape", self.userExit) self.taskMgr.add(self.cubeMotionTask, "CubeMotionTask") def cubeMotionTask(self, task): self.cube.setPos(0, 20, task.time * 0.2) return task.cont app = HelloWorld() app.run()
- 立方体が自動で上方向に動き続けることを確認する。
《ヒント》
- マウス操作で視点を引いて、立方体が画面外まで上がっていく様子を追跡する。
task.time * 0.2の0.2を大きくすると(例:1.0)速く、小さくすると(例:0.05)遅く上昇する。
《考察ポイント》
- 演習8との違い:演習8ではキーを押すたびに1回だけ動いた。本演習ではキーを押さなくても、フレームごとに位置が更新され、なめらかに動く。
- 本演習では、位置が時間の式(z = time × 0.2)であらかじめ固定されている。そのため軌道(どう動くか)は実行前から決まっており、途中で変えられない。次の演習では「速度」という状態を介して位置を更新し、入力で軌道を変えられるようにする。これが演習9と演習10の本質的な違いである。
演習10: 速度を持たせて動かす
位置を直接書き換えるのではなく、速度を持たせて、フレームごとに位置を進める。物理シミュレーションやゲーム制御の出発点である。
本演習では「位置 += 速度 × 経過時間」という式で立方体を動かす(+=はPythonの加算代入演算子で、左辺の値に右辺を加えて左辺に戻す)。速度(vx, vy, vz)は矢印キーで増減させ、位置(x, y, z)は毎フレーム自動更新する。経過時間はglobalClock.getDt()で取得する(概念2、概念3を参照)。
設計の考え方:位置(x, y, z)と速度(vx, vy, vz)をPython側の変数として保持する。毎フレーム、これらの変数で次の位置を計算し、その結果をsetPos()でモデルに反映する。setPos(0, 20, 0)で与えた初期位置と、変数self.x, self.y, self.zの初期値(0, 20, 0)が一致しているのはこのためである。計算はPython側の変数で行い、モデルへの反映は計算結果を渡すだけ、という役割分担になっている。
注意:同じキーを複数回押すと加速する。反対方向のキーを押すと減速し、さらに押すと逆向きに動く。
《手順》
- 次のコードを実行する。
from direct.showbase.ShowBase import ShowBase class HelloWorld(ShowBase): def __init__(self): ShowBase.__init__(self) self.scene = self.loader.loadModel("models/environment") self.scene.reparentTo(self.render) self.cube = self.loader.loadModel("models/misc/rgbCube") self.cube.reparentTo(self.render) self.cube.setPos(0, 20, 0) self.cube.setHpr(45, 0, 0) self.x = 0 self.y = 20 self.z = 0 self.vx = 0 self.vy = 0 self.vz = 0 self.accept("arrow_up", self.up_key) self.accept("arrow_down", self.down_key) self.accept("arrow_right", self.right_key) self.accept("arrow_left", self.left_key) self.accept("escape", self.userExit) self.taskMgr.add(self.cubeMotionTask, "CubeMotionTask") def cubeMotionTask(self, task): dt = globalClock.getDt() self.x += self.vx * dt self.y += self.vy * dt self.z += self.vz * dt self.cube.setPos(self.x, self.y, self.z) return task.cont def up_key(self): self.vz = self.vz + 1 def down_key(self): self.vz = self.vz - 1 def right_key(self): self.vx = self.vx + 1 def left_key(self): self.vx = self.vx - 1 app = HelloWorld() app.run()
- 立方体が表示されることを確認する。最初は速度がゼロのため停止している。
- 矢印キーを押すと立方体が動き出し、キーから手を離しても動き続けることを確認する。同じキーをもう一度押すと加速し、反対のキーを押すと減速する。
《ヒント》
- 上キーを2〜3回押した後、停止しない様子を観察する。
- 下キーを押して減速させ、速度をゼロにできるかを試す。
- 上キーと右キーを組み合わせると、斜め方向へ動く。
《考察ポイント》
- 演習8(位置を直接動かす)と本演習(速度を介して動かす)を比較する。前者は1単位ずつ移動する離散的な動き、後者は慣性を持つ連続的な動きである。慣性の有無が表現の違いを生む。
- 速度に経過時間(
dt = globalClock.getDt())を掛けているため、フレームレートが変わっても、立方体の動く速さは一定に保たれる(概念3を参照)。フレームが速いときはdtが小さく1回の移動量も小さい、遅いときはdtが大きく1回の移動量も大きい、という形で相殺される。 - ここに重力を加えれば落下、摩擦を加えれば自然な停止が表現できる。本演習の枠組みは、ゲームや物理シミュレーションの出発点である。