前回の記事「PythonでOpenGLを理解する」で学習の基礎を固めたので、今回はPyGameとPyOpenGLを使ってOpenGLに飛び込んでみましょう。
PyOpenGLはPythonとOpenGL APIの間のブリッジとして使われる標準化されたライブラリで、PyGameはPythonでゲームを作るために使われる標準化されたライブラリです。
便利なグラフィカルライブラリやオーディオライブラリを内蔵しており、記事の最後ではこれを使ってより簡単に結果をレンダリングすることになります。
前回の記事で述べたように、OpenGLは非常に古いものなので、その正しい使い方や理解のためのチュートリアルをオンラインで見つけることはあまりできません。
なぜなら、どのトッププレイヤーもすでに新しい技術にどっぷりと浸かってしまっているからです。
この記事では、あなたが知っておく必要があるいくつかの基本的なトピックに飛びます。
- PyGameを使ったプロジェクトの初期化
- オブジェクトの描画
- 反復アニメーション
- 変換行列の利用
- 複数の変換を実行する
- 実装例
PyGameを使ったプロジェクトの初期化
まず最初に、PyGameとPyOpenGLをインストールする必要があります(まだインストールしていない場合)。
$ python3 -m pip install -U pygame --user
$ python3 -m pip install PyOpenGL PyOpenGL_accelerate
注:より詳細なインストール方法は、前回のOpenGLの記事で紹介しています。
もし、インストールに関して問題があれば、PyGameの “Getting Started “のセクションを参照するとよいでしょう。
本3冊分のグラフィックス理論を読んでも意味がないので、PyGameライブラリを使って先手を打ちましょう。
これは本質的に、プロジェクトの初期化から実際のモデリングやアニメーションまでのプロセスを短縮するだけです。
まず始めに、OpenGLとPyGameの両方から必要なものをインポートする必要があります。
import pygame as pg
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
次に、初期化に取り掛かります。
pg.init()
windowSize = (1920,1080)
pg.display.set_mode(display, DOUBLEBUF|OPENGL)
初期化は3行のコードだけですが、それぞれは少なくとも簡単な説明に値します。
-
pg.init()
: PyGameの全モジュールの初期化 – この関数は天の恵みです。 -
windowSize = (1920, 1080)
: 固定のウィンドウサイズを定義する -
pg.display.set_mode(display, DOUBLEBUF|OPENGL)
: ここでは、OpenGLをダブルバッファリングで使用することを指定しています。
ダブルバッファリングとは、任意の時点で2つの画像が存在することを意味します – 1つは私たちが見ることができ、もう1つは私たちが適当に変換することができます。
2つのバッファが入れ替わったときに、変換によって生じる実際の変化を見ることができます。
ビューポートの設定ができたので、次は何を見るか、つまり、「カメラ」をどこに置いて、どこまで、どこまで見ることができるかを指定する必要があります。
これはフラストラムと呼ばれるもので、カメラの視界(見えるものと見えないもの)を視覚的に表現する切り取られたピラミッドにすぎません。
フラストラムは4つの重要なパラメータで定義されます。
- FOV(視野角)。角度(度
- アスペクト比(Aspect Ratio)。幅と高さの比率で定義されます。
- クリッピングプレーン付近のz座標。最小描画距離
- 遠方クリッピングプレーンのz座標。最大描画距離
では、これらのパラメータを考慮して、OpenGLのCコードでカメラを実装してみましょう。
void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
gluPerspective(60, (display[0]/display[1]), 0.1, 100.0)
フラストレートがどのように機能するかを理解するために、参考画像を掲載します。
近傍平面と遠方平面を使用することで、より良いパフォーマンスが得られます。
現実的には、視野外のものをレンダリングすることは、実際に見えるものをレンダリングするために使われるハードウェアパフォーマンスの浪費になります。
そのため、プレイヤーに見えないものはすべて、視覚的に存在しないにもかかわらず、暗黙のうちにメモリに保存されます。
矩形領域内のみのレンダリングがどのように見えるかを示す素晴らしいビデオがあります。
反復アニメーション
このプログラムを “killable “にするためには、以下のコードを挿入する必要がある。
cubeVertices = ((1,1,1),(1,1,-1),(1,-1,-1),(1,-1,1),(-1,1,1),(-1,-1,-1),(-1,-1,1),(-1, 1,-1))
これは基本的にPyGameのイベントをスクロールするリスナーで、私たちが「kill window」ボタンをクリックしたことを検出すると、アプリケーションを終了させます。
PyGameのイベントの詳細については、今後の記事で説明します。
このイベントをすぐに導入したのは、アプリケーションを終了したいときに毎回タスクマネージャーを起動しなければならないのは、ユーザーやユーザー自身にとってかなり不快なことだからです。
この例では、ダブルバッファリングを使っています。
これは、2つのバッファ(描画用のキャンバスと考えてください)を使って、一定の間隔で入れ替え、動いているように見せかけるということです。
このことから、コードは次のようなパターンになるはずです。
cubeEdges = ((0,1),(0,3),(0,4),(1,2),(1,7),(2,5),(2,3),(3,6),(4,6),(4,7),(5,6),(5,7))
-
glClear
: 指定されたバッファ (キャンバス) をクリアする関数です。この場合、カラーバッファ (生成されたオブジェクトを描くための色情報を含む) と深度バッファ (生成されたすべてのオブジェクトの前方または後方の関係を格納するバッファ) をクリアする関数です。 -
pg.display.flip()
: アクティブなバッファの内容でウィンドウを更新する関数です。 -
pg.time.wait(1)
: プログラムを一定時間停止させる関数です。
なぜなら、glClear
を使わないと、すでに描かれているキャンバス(この場合はスクリーン)の上に絵を描くだけになってしまい、結局はぐちゃぐちゃになってしまうからです。
次に、アニメーションのように画面を継続的に更新したい場合は、すべてのコードを while
ループの中に記述する必要があります。
-
- イベントを処理する(この場合、終了するだけ)。
-
- 色と深度のバッファをクリアして、再び描画できるようにする。
- オブジェクトの変形と描画
- 画面の更新
- GOTO 1.
このようなコードになるはずです。
cubeQuads = ((0,3,6,4),(2,5,6,3),(1,2,5,7),(1,0,4,7),(7,4,6,5),(2,3,0,1))
変換行列の活用
前回は、理論上、参照点を持つ変換を構成する必要があることを説明しました。
OpenGLも同じように動作し、次のコードで見ることができます。
def wireCube():
glBegin(GL_LINES)
for cubeEdge in cubeEdges:
for cubeVertex in cubeEdge:
glVertex3fv(cubeVertices[cubeVertex])
glEnd()
この例では、xy平面上でz軸の回転を行い、回転の中心を (1,1,1)
として30度ずつ回転させました。
これらの用語が少しわかりにくいようであれば、少し復習しておきましょう。
- z軸回転とは、z軸の周りを回転することです。
2.z軸回転とは、z軸を中心に回転させることです これは、2次元平面を3次元空間に近似させることを意味し、この変換全体は基本的に2次元空間の参照点を中心に通常の回転を行うようなものです。
>
2. 2. 3次元空間全体をz=0の平面に押し込んでxy平面を得る(あらゆる方法でzパラメータを排除する)。
3. 3. 回転中心は、与えられたオブジェクトを回転させる頂点です (デフォルトの回転中心は原点の頂点(0,0,0)` です)。
OpenGLは、常に1つのグローバルな変換行列を記憶し、修正することで、上記のコードを理解します。
つまり、OpenGLで何かを書くとき、あなたが言っていることは、次のようなことです。
def solidCube():
glBegin(GL_QUADS)
for cubeQuad in cubeQuads:
for cubeVertex in cubeQuad:
glVertex3fv(cubeVertices[cubeVertex])
glEnd()
なぜなら、時にはソースコード全体ではなく、1つのオブジェクトに対して変換を利用したいことがあるからです。
これは低レベルのOpenGLでバグが発生する非常に一般的な理由です。
OpenGLのこの問題のある機能に対処するために、私たちはプッシュとポップの変換行列を提供します – glPushMatrix()
と glPopMatrix()
です。
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
quit()
これらは、単純なLIFO(Last-in-First-Out)の原則で動きます。
言い換えれば、終わった後に廃棄できるローカル行列を作成することで、このブロックで実行するすべての変換を分離するのです。
オブジェクトが変換されたら、変換行列をスタックからポップし、残りの行列はそのままにします。
多重変換の実行
OpenGL では、前述のように、変換は、変換行列のスタックの最上位にあ るアクティブな変換行列に追加されます。
これは、変換が逆順に実行されることを意味します。
handleEvents()
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
doTransformationsAndDrawing()
pg.display.flip()
pg.time.wait(1)
この例では、Object1 が最初に回転され、次に平行移動され、Object2 が最初に平行移動され、次に回転されます。
最後の 2 つの概念は、実装例では使用しませんが、シリーズの次の記事で実用的に使用されます。
実装例
以下のコードは、立方体を画面上に描画し、 (1,1,1)
ベクトルを中心に 1 度ずつ連続的に回転させるものです。
また、cubeQuads
を cubeEdges
に置き換えることで、ワイヤーキューブを描くように非常に簡単に変更することができます。
while True:
handleEvents()
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
doTransformationsAndDrawing()
pg.display.flip()
pg.time.wait(1)
このコードを実行すると、PyGame ウィンドウが表示され、キューブのアニメーションがレンダリングされます。
結論
ライティング、テクスチャ、高度なサーフェスモデリング、コンポジットモジュラーアニメーションなど、OpenGLについて学ぶべきことはまだまだたくさんあります。
しかし、心配はいりません。
これらすべては、次の記事で、OpenGLについて一般の人たちに、一からきちんと教えるために説明されるからです。
そして、心配しないでください、次回の記事では、実際に何かまともな絵を描きますよ。