Panda3D 3次元ゲームエンジン入門

【概要】Panda3Dはオープンソースの3次元ゲームエンジンである。本記事では、Pythonプログラムから3次元空間を生成し、モデルを配置し、照明を当て、キーボードやマウスで操作する流れを、10の演習を通して学ぶ。

【前提知識】Pythonの基本文法(クラス、継承、メソッド定義)。

【学修のねらい】3次元コンピュータグラフィックスの基本要素(座標系、視点、モデル、変換、照明、イベント、アニメーション、運動)を、演習を通して理解する。

【目次】

  1. はじめに
  2. 本教材を読む前に:3つの基本概念
  3. 前準備
  4. 演習1: 画面を開く
  5. 演習2: マウスによる視点操作
  6. 演習3: 複数の3次元モデルの読み込み
  7. 演習4: 3次元モデルの配置
  8. 演習5: 照明を当てる
  9. 演習6: キーボードへの反応
  10. 演習7: マウスクリックへの反応
  11. 演習8: 矢印キーで立方体を動かす
  12. 演習9: 立方体を自動で動かす
  13. 演習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文や自分での定義なしに利用できる。本教材で使うものは次の通りである。

globalClockself.を付けずに使うグローバル変数である。コード中に定義が見当たらなくても誤りではない。

概念3:フレームとアニメーション

Panda3Dは1秒間に何回も画面を描き直している。その1回分を「フレーム」と呼ぶ。毎フレーム、モデルの状態(位置など)を少しずつ更新し続けると、人の目にはなめらかに動いて見える。これがアニメーションの基本原理である。

1フレームにかかる時間は一定とは限らない。パソコンの性能や処理の重さによって、フレームの間隔は変動する。そこで動かす量を計算するときは、移動の速さに前フレームから今フレームまでの経過秒数(dt)を掛ける。フレームが速く描かれるとき(dtが小さい)は1回あたりの移動量を小さく、遅いとき(dtが大きい)は大きくすることで、実時間あたりの動く速さが一定に保たれる。この考え方は演習10で使う。

前準備

Pythonのインストール

本教材はPythonが動作する環境を前提とする。Pythonが未導入の場合は事前にインストールする。

Panda3Dのインストール

本記事の演習はPanda3Dを使う。Panda3Dは3次元コンピュータグラフィックスのためのライブラリである。インストールは1回だけ行う。

コマンドプロンプトを管理者権限で起動する(Windowsキー → 「cmd」と入力 → 右クリック → 「管理者として実行」)。起動したコマンドプロンプトに次を入力して実行する。

pip install panda3d
Panda3Dのインストール画面

プログラムの実行

各演習のコードを実行する方法は2通りある。

方法A:Visual Studio Codeで実行する。Visual Studio Codeがインストール・設定済みであれば、本ページのコードを編集画面に貼り付け、実行する。

方法B:ファイルに保存して実行する。テキストエディタを開き、本ページのコードを貼り付ける。a.pyのようなファイル名で保存する。コマンドプロンプトでそのファイルがあるフォルダに移動し、次を入力して実行する。

python a.py

以下、各演習では「コードを実行する」と記述する。方法AまたはBで実行する。プログラムを終了するときは、表示されたウィンドウ右上の「×」をクリックする。

演習1: 画面を開く

最小構成のプログラムで画面を表示し、Panda3Dが動作することを確認する。

《手順》

  1. 次のコードを実行する。
    from direct.showbase.ShowBase import ShowBase
    
    class HelloWorld(ShowBase):
    
        def __init__(self):
            ShowBase.__init__(self)
    
    app = HelloWorld()
    app.run()
    
    Panda3Dの基本コード
  2. 何も描かれていない灰色のウィンドウが表示されることを確認する。
    Panda3Dの初期画面(灰色の空の画面)

《ヒント》

《考察ポイント》

演習2: マウスによる視点操作

3次元モデルを1つ読み込んで表示し、マウスで視点を動かす。

《手順》

  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"のような相対指定で読み込める。

  2. 竹林のような3次元シーンが表示されることを確認する。
    環境モデルの表示結果(竹林のシーン)
  3. マウスで視点を動かす。
    • 左ボタンを押しながらドラッグ:視点の向きを変える(上下左右)。
    • 右ボタンを押しながらドラッグ:視点を前後に動かす(ズームに相当)。

《ヒント》

《考察ポイント》

演習3: 複数の3次元モデルの読み込み

2つのモデルを同じシーンに読み込む。

Panda3Dには複数の標準モデルが同梱されており、いずれも"models/..."という相対指定で読み込める。本演習で使う2つのモデルは次の通りである。

《手順》

  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)
    
            self.cube = self.loader.loadModel("models/misc/rgbCube")
            self.cube.reparentTo(self.render)
            self.cube.setPos(0, 20, 0)
    
    app = HelloWorld()
    app.run()
    
    2つのモデルを読み込むコード
  2. 竹林のシーンの中に立方体が表示されることを確認する。マウスで視点を変えて、立方体を複数の角度から観察する。
    竹林と立方体が表示された画面

《ヒント》

《考察ポイント》

演習4: 3次元モデルの配置

モデルの位置・大きさ・向きを制御する。3次元コンピュータグラフィックスの基本変換である移動・スケール・回転を順に実施する。

演習4-1: 位置を指定して配置

Panda3Dの座標系:xは左右(右が+)、yは前奥(カメラから見て奥が+)、zは上下(上が+)。z-up(zを上方向とする)の右手座標系である。

Panda3Dの座標系(x:左右、y:前奥、z:上下)

《手順》

  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)
    
            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つの立方体を異なる位置に配置するコード
  2. 2つの立方体が画面の左右に離れて表示されることを確認する。マウスで視点を動かして位置関係を確かめる。
    異なる位置に配置された2つの立方体

《ヒント》

《考察ポイント》

演習4-2: setScale()で拡大縮小

setScale(値)でモデルそのものの大きさを変える。1.0が基準(等倍)である。

《手順》

  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)
    
            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()
    
    setScaleで拡大縮小するコード
  2. 右側の立方体が2.4倍に拡大されていることを確認する。
    右側の立方体が2.4倍に拡大された画面

《ヒント》

《考察ポイント》

演習4-3: setHpr()で回転

setHpr(H, P, R)でモデルの向き(回転)を変える。H・P・Rはオイラー角と呼ばれ、単位は度である。飛行機の姿勢にたとえると次のようになる。

setHpr(45, 0, 0)は「z軸まわりに45度回す」という意味になる。

《手順》

  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)
    
            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()
    
    setHprで回転を設定するコード
  2. 左側の立方体(回転なし)と、右側の立方体(z軸まわりに45度回転)を見比べる。
    右側の立方体が回転した状態で表示された画面

《ヒント》

《考察ポイント》

演習5: 照明を当てる

これまでの演習では、モデルは平坦に見えていた。Panda3Dでは初期状態で照明が当たっていないため、面の向きによる明暗が表現されない。本演習では照明を追加し、立体感を与える。

本演習で使う2種類の光:

光は次の3ステップで適用する。光のオブジェクトを作り、render.attachNewNode()でシーングラフに取り付け、render.setLight()でシーン全体に適用する。

《手順》

  1. 次のコードを実行する。
    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を参照)。以降の演習でもこの目的で傾けて表示する。

  2. 立方体や竹林のシーンに明暗がつき、立体的に見えることを確認する。照明を入れる前(演習4-3)と見比べる。

《ヒント》

《考察ポイント》

演習6: キーボードへの反応

ユーザーの入力に反応するシーンを作る。まずキーボード入力に対応させる。

self.accept(キー名, 関数)は、指定したキーが押されたときに、その関数を呼び出すよう登録する(イベントハンドラの登録)。

キー名の例:"a", "space", "enter", "escape", "arrow_left", "arrow_up", "arrow_down", "arrow_right"

本演習からは、"escape"キーでプログラムを終了できるようにする(self.userExitはアプリケーションを終了するメソッドである)。

《手順》

  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)
    
            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()
    
    キーボードイベントを処理するコード
  2. 立方体が表示されることを確認する。
    立方体の初期表示状態
  3. キーボードの「A」キーを押すと、立方体が拡大されることを確認する。
    Aキー押下後に拡大された立方体
  4. 「Esc」キーを押すと、プログラムが終了することを確認する。

《ヒント》

《考察ポイント》

演習7: マウスクリックへの反応

マウスクリックで立方体を変化させる。

マウスボタンの名前:"mouse1"(左)、"mouse2"(中)、"mouse3"(右)

《手順》

  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)
    
            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. 立方体が表示されることを確認する。
    立方体の初期表示状態
  3. マウスの右ボタンをクリックすると、立方体が縮小されることを確認する。
    右クリック後に縮小された立方体
  4. マウスの左ボタンをクリックすると、立方体が拡大されることを確認する。
    左クリック後に拡大された立方体

《ヒント》

《考察ポイント》

演習8: 矢印キーで立方体を動かす

現在の位置を取り出し、値をずらして、書き戻す処理を繰り返すことで、立方体を移動させる。

《手順》

  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)
    
            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()
    
    矢印キーでオブジェクトを移動するコード
  2. 立方体が表示されることを確認する。
  3. 矢印キー(上、下、右、左)を押すと、立方体が1単位ずつ移動することを確認する。
    矢印キーで移動した立方体

《ヒント》

《考察ポイント》

演習9: 立方体を自動で動かす

キー入力を待たずに、毎フレーム自動で位置を更新する。

self.taskMgr.add(関数, 名前)で関数を登録すると、その関数が毎フレーム自動的に呼び出される(このような関数を「タスク」と呼ぶ)。タスク関数の引数taskには現在のタスクの状態が入っており、task.timeはタスクが最初に実行されてからの経過秒数である(本演習では起動直後にタスクを登録するため、アプリ起動からの経過秒数とみなしてよい)。task.contは「次のフレームも継続して呼ぶ」ことを示す戻り値である(フレームについては概念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)
    
            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()
    
    タスクでオブジェクトを自動移動するコード
  2. 立方体が自動で上方向に動き続けることを確認する。
    自動移動開始時の立方体の位置
    自動移動後の立方体の位置

《ヒント》

《考察ポイント》

演習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側の変数で行い、モデルへの反映は計算結果を渡すだけ、という役割分担になっている。

注意:同じキーを複数回押すと加速する。反対方向のキーを押すと減速し、さらに押すと逆向きに動く。

《手順》

  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)
    
            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. 矢印キーを押すと立方体が動き出し、キーから手を離しても動き続けることを確認する。同じキーをもう一度押すと加速し、反対のキーを押すと減速する。

《ヒント》

《考察ポイント》