Pygletによる「手抜き」OpenGL入門

総目次

1. 準備

1.1 ソフトウェアのセットアップ

これから登場するコードを実行して試すにはPythonの動作するパソコン環境が必要である。 Google Colaboratoryなどのクラウド環境では動作しないので注意。 自分のパソコンにPythonをインストールする方法については、ネットで検索するなどして調べること。 いまどきは、編集や実行にはVisual Studio Codeを使うのがお薦めである。

自分のPC上で動作しているJupyter NotebookやJupyter Labでも動作させることはできるが、プログラムの終了処理がうまくいかないことがあるようだ。 macOS上のJupyter Notebookの場合、コードに %gui osx の1行を加えておくとよい。

Pygletのアイコンは豚のしっぽをイメージしている気配があるので、piglet(子豚)に引っ掛けているとすると、 「ピグレット」と呼ぶのが正しいのかもしれない。

次に、Python環境に Pyglet (ネット上の動画をみていると「ピグレット」または「パイグレット」と発音しているようだ)を追加インストールする。 WindowsのPCではコマンドプロンプトを、macOSやLinuxではターミナルを開いて

pip install pyglet

を実行すると、必要なソフトが自動的にインストールされる。 インストール後に最新版に更新するには

pip install -U pyglet

とすればよい。Pygletは不具合の修正や機能拡張のためにしばしば更新されるので、このサイトの例題がうまく動作しないときは、 更新作業を行ってみるとよい。 ここで、pipのコマンド名は、pip3等、インストールの状況によって変わるので注意。

そのほか、「モジュールが見つからない」といったエラーが出たら、pipコマンド等を使って、適宜追加インストールすること。

現時点で、Pygletにはバージョン1系と2系が存在し、互換性が無いところが多くある。 ここでは専らPygletのバージョン2系について説明する。 また、古いバージョンのPythonでは動作しないコードも一部に含まれる。 できるだけPython 3.10以上を使用すること。

PygletのインストールTIPS

Python環境は、Linuxなら apt や rpm、macOSなら homebrew などのパッケージ・マネージャーを使ってインストールするのが常であるが、 pipコマンドを使ってpygletを追加でインストールしようとすると

error: externally-managed-environment
× This environment is externally managed

等と出て、出鼻をくじかれてしまうかもしれない。これは、パッケージ・マネージャーに標準的にPygletが含まれていないためである。

そんなときは、(エラーメッセージにも書かれているとおり)自分専用の仮想環境(virtual environment)を作成し、その中でPygletをインストールする必要がある。 詳しくは、「python 仮想環境」で検索のこと。 機械学習やAI関係にも興味がある方は、この際 Anaconda をインストールしておくと、機械学習関係のパッケージが整備された仮想環境が簡単に手に入って幸せになれるかもしれない。

それ以外の方法として、venv というモジュールを使ってPythonの仮想環境を作成し、そこにPygletをインストールするまでのコマンドの例を以下に示す:

Ubuntu Linuxの場合
sudo apt install python3
sudo apt install python3-venv
python3 -m venv ~/py3
source ~/py3/bin/activate
pip3 install pyglet
macOS (Homebrew)の場合
brew install python3
brew install python3-venv
python3 -m venv ~/py3
source ~/py3/bin/activate
pip3 install pyglet

Visual Studio Codeを使っている場合、MicrosoftのPython拡張機能をインストールしておくと良い。 拡張機能からPygletをインストールしておいた仮想環境を使うためには、コマンドパレット(ウィンドウ上部の「検索」をクリックして、「コマンドの表示と実行>」を選択)で

Python: Select Interpreter

を実行するか、Pythonのファイルを開いている状態で、ウィンドウ右下の通知(🔔)のマークの左隣りをクリックすると、Pythonの実行ファイルの選択(あるいは新規入力)が求められるので、 仮想環境の実行ファイルへのパス、例えば

~/py3/bin/python3

等と入力しておく。

1.2 動作確認

ソフトウェアの設定が終わったら、早速、以下のPythonコードを実行してみよう:

prog-1-1.py

import pyglet

print(pyglet.gl.gl_info.get_version())
print(pyglet.gl.gl_info.get_vendor())
print(pyglet.gl.gl_info.get_renderer())
print(pyglet.gl.gl_info.get_extensions())

ターミナル(Visual Studio Codeの場合は、通常、ウィンドウの下側の区画に表示される)に、細かい内容はOSや使用環境によって異なるものの、 だいたい以下のような文字列が出力されれば成功である。

(4, 2)
Microsoft Corporation
D3D12 (AMD Radeon(TM) Graphics)
{'GL_ARB_texture_buffer_range', 'GL_ARB_ES3_compatibility', .... }

ここで、最初の行の(4,2)はPCに備わっているOpenGLのバージョンを表している。この場合はバージョン4.2となる。 以下では、OpenGLのバージョン3.3以上を想定して書かれているので、 最初の行が(3,3), (4,0), (4,1)... であることをまず確認しておくこと。

1.3 OpenGLの動作の概要

具体的なコーディングに入る前に、OpenGLとはどのような仕組みなのかについて、Pygletの使用を想定しつつ、概要をまず把握しておこう。

ウィンドウとコンテクスト

Pygletを使うと、パソコン上に表示画面(ウィンドウ)を開いて、そこに図形や画像を表示することができる。 このとき、ウィンドウはひとつの仮想的な表示装置のように見なされ、OpenGLの命令(API関数の呼び出し)を行う都度、その状態が変化し、それが保持される。 例えば、ウィンドウの背景色をglClearColor()関数で一旦設定すると、変更しない限り、その設定は活きている。 であるから、何かの都合でOpenGLの設定(例えば、色のブレンディング)を変更したら、作業後にそれを元に戻しておかないと、別の箇所に影響が出てしまう可能性がある。 こうした一連の動作をOpenGLではコンテクストと呼んでいる。

Pygletでは、複数のウィンドウを開いて、それぞれ異なるコンテクストを割り当てることも可能である。その場合は、ウィンドウオブジェクトを生成した後で、

(Windowオブジェクト名).switch_to()

のようにコンテクストを切り替える必要がある。ただし、この解説では、表示ウィンドウは1つ(よってコンテクストも1つ)の場合のみを扱うことにする。

シェーダーとその役割り

OpenGLでは、点、線分、三角形という基本的な図形要素(プリミティブ)によって描画が行われる。 そして、座標計算やラスター化(幾何学的な図形情報を画素に還元して表現すること)等、描画に関わる処理のほとんどは、シェーダーと呼ばれるプログラムによって行われる。 シェーダーはGLSLと呼ばれる(Pythonとは異なる)言語で記述され、パソコンに内蔵されているGPU(画像処理プロセッサー)で実行される。 シェーダーのプログラムは、Pythonプログラムの中に文字列として記述し、Pythonコードの実行の都度、OpenGLの機能を使ってコンパイルされ、GPU側に渡される。

そのため、Pygletを使ったPythonのコードの仕事は、OpenGLの動作設定と、シェーダーとのデータの受け渡し(バッファー)の管理、そして、 バッファーを使って、図形の頂点座標や色、必要な座標変換についての情報をシェーダーに受け渡すことにある。 加えて、マウスやキーボード操作を補足して適切な処理(イベントハンドリング)を行うのも、Pythonコードの役割りである。

この解説の例題に限れば、Pythonの部分が半分、シェーダー(GLSL)の部分が半分くらいのコーディング量となるだろう。 ただし、GLSLについての詳細な説明はほとんど割愛(「手抜き」)し、要点を述べるのみとする。


2. ウィンドウを表示して2次元図形を描く

2.1 ウィンドウの表示

最初の例として、横幅400ピクセル、高さ300ピクセルのウィンドウを開いて、暗いスレートブルーに塗りつぶすコードを示す。

prog-2-1.py


import pyglet

window = pyglet.window.Window(width=400,height=300)
pyglet.gl.glClearColor(0.3, 0.3, 0.5, 1.0)

@window.event
def on_draw():
    window.clear()

pyglet.app.run()

prog-2-1.pyの実行結果

プログラムの各行の意味や動作は、Pythonの経験があれば、だいたい予想がつくだろう。

window = pyglet.window.Window(width=400,height=300)で、描画域が400x300のウィンドウオブジェクトを生成する

同様の手順で、複数のウィンドウを同時に生成することも可能ではあるが、コンテクストの管理が必要となり煩雑なので、 この解説ではウィンドウはひとつだけの場合を前提に、話しを進めることにする。

Pygletの機能を使うには、コードの最初のあたりに import pyglet と書いて、モジュールを呼び出しておく必要がある。

pyglet.gl.glClearColor(0.3, 0.3, 0.5, 1.0)は、画面をクリアした際の背景色を指定する関数である。 4つの引数は、光の三原色のそれぞれの強度(R, G, B)と、不透明度(アルファ)の程度を、0から1の範囲で表している。 アルファは、色を塗り重ねた場合の上に重ねる色の配合割合を表し、0が透明(下地がそのまま)、1が完全不透明(描画色)に対応する。 この場合、色は暗いスレートブルーで、不透明度は1を表す。

Pygletでは、描画が必要となったタイミングで、イベント(ソフトウェア的な「合図」)が発生し、指定した関数が呼び出される。 @window.eventは、次の行で定義する関数をWindowオブジェクトのイベント処理に用いることを表す指示(デコレータ)である。 WindowクラスはEventDispatcherを継承しており、EventDispatcherクラスのevent()というメソッドに 決められた名前の関数を渡すことで、イベント処理用関数(イベントハンドラー)が設定される仕組みになっている。 この例では、画面の書き換えが必要になった際に自動的に呼び出されるイベントハンドラーon_draw()を定義している。

デコレーター@window.eventの中のwindowは生成したウィンドウオブジェクトを指す。 よって、もしもwin = pyglet.window.Window(...)のようにしてウィンドウを生成したのなら、デコレーターは @win.eventとなる。

このon_draw()は最も基本的なハンドラーであるが、他にも、以下のハンドラーが設定可能である:

on_key_press(symbol, modifiers)
on_key_release(symbol, modifiers)
on_text(text)
on_text_motion(motion)
on_text_motion_select(motion)
on_mouse_motion(x, y, dx, dy)
on_mouse_drag(x, y, dx, dy, buttons, modifiers)
on_mouse_press(x, y, buttons, modifiers)
on_mouse_release(x, y, buttons, modifiers)
on_mouse_scroll(x, y, scroll_x, scroll_y)
on_close()
on_mouse_enter(x, y)
on_mouse_leave(x, y) 
on_expose()
on_resize(width,height)
on_move(x,y)
on_activate()
on_deactivate()
on_show()
on_hide()
on_context_lost()
on_context_state_lost()

これらのイベントハンドラーを使いながら、キーボードやマウスによる操作を実現することができる。 具体的な機能や使い方は、必要になった都度、説明することにするが、関数名からそれぞれの役割は自ずと察することができるだろう。

pyglet.app.run()を実行することによって、描画とイベントの処理が開始される。

2.2 色付きの三角形の表示

では、ウィンドウの内部に、頂点座標(-0.9, -0.9), (0.9, -0.9), (0.0, 0.9)の三角形を描画してみよう。 Pygletでは、多角形や円などの基本的な2次元図形を描画するための機能を備えているが、ここでは敢えてOpenGLの基本的(原始的?)な機能を 使った方法を試してみることにする。 具体的なコードは以下の通りである。

prog-2-2.py

import pyglet
from pyglet.gl import *
from pyglet.math import Mat4

# シェーダーの定義
vertex_source = """#version 330 core
    in vec2 position;
    in vec4 colors;
    out vec4 vertex_colors;

    uniform WindowBlock
    {
        mat4 projection;
        mat4 view;
    } window;

    uniform mat4 model ;

    void main()
    {
        gl_Position = window.projection * window.view * model * vec4(position, 0.0, 1.0) ;
        vertex_colors = colors;
    }
"""

fragment_source = """#version 330 core
    in vec4 vertex_colors;
    out vec4 final_colors;

    void main()
    {
        final_colors = vertex_colors;
    }
"""

# ウィンドウの生成
window = pyglet.window.Window(width=400,height=300)
glClearColor(0.3, 0.3, 0.5, 1.0)

# シェーダーのコンパイル
vert_shader = pyglet.graphics.shader.Shader(vertex_source, 'vertex')
frag_shader = pyglet.graphics.shader.Shader(fragment_source, 'fragment')
shader = pyglet.graphics.shader.ShaderProgram(vert_shader, frag_shader)
batch = pyglet.graphics.Batch()

# 頂点リストの生成
vlist = shader.vertex_list(3, GL_TRIANGLES, batch=batch)

# 描画用イベントハンドラーの定義
@window.event
def on_draw():
    window.clear()
    window.viewport = (0, 0, 400, 300)
    window.projection = Mat4()
    window.view = Mat4()
    shader['model'] = Mat4()
    vlist.position = (-0.9, -0.9, 0.9, -0.9, 0.0, 0.9)
    vlist.colors = (1.0, 0.0, 0.0, 1.0,  0.0, 1.0, 0.0, 1.0,  0.0, 0.0, 1.0, 1.0)
    batch.draw()

# イベントループ開始
pyglet.app.run()

prog-2-2.pyの実行結果

prog-2-1.pyと比べるとかなりコードが長くなっているが、基本的な構造は同じである。冒頭から順にその中身をみてみよう:

冒頭部分

このプログラムでは、冒頭部分で from pyglet.gl import * として、OpenGL関係の定義をより簡便に 呼び出せるようにしてある。これにより、例えば、prog-2-1.pyのpyglet.gl.glClearColor(0.3, 0.3, 0.5, 1.0)のところは、単に glClearColor(0.3, 0.3, 0.5, 1.0)と済ませることができる。

加えて、行列計算のため from pyglet.math import Mat4 をインポートしている。 ここで、Mat4は4行4列の行列を操作するためのクラスである。

シェーダーの定義

グラフィック表示の仕組みは少しややこしいところがあって、CPUが処理するコード(この場合はPython)と、 グラフィック表示用のデバイスであるGPUが処理するコードの2本立てで構成される。 そして、後者のことをシェーダー(shader)プログラムと呼ぶ習わしになっている。 画像に陰影をつけることをシェーディングと呼ぶが、シェーダーは陰影処理に限らずGPUでの処理をC言語に似たスタイルで記述する専用プログラムである。

通常、CPU側で処理するコード(この場合はPython)の中に、シェーダーのコードをデータとして含めておいて、GPU側に処理させる方式をとる。

シェーダーは複数のプログラムが数珠つなぎ(パイプライン)式に連携して動作する仕組みになっており、前段の頂点シェーダ(vertex shader)がデータの入口に、 後段のフラグメントシェーダ(fragment shader)が描画内容の出力を受け持つ取り決めになっている。 必要に応じて、その間にさらに別のシェーダーを介在させることもできるが、ここでは説明は割愛する。 最低限でも頂点シェーダとフラグメントシェーダは必要となる。

prog-2-2.pyでは、vertex_source = """何々"""が頂点シェーダー、fragment_source = """何々"""の箇所が フラグメントシェーダーのコードである。

ウィンドウの生成

この部分は、前掲のprog-2-1.pyと全く同様である。

シェーダーのコンパイル

この部分で、2つの文字列 vertex_sourcefragment_sourceに格納されているシェーダープログラムを パソコンに装着されているGPU用にコンパイル(コード変換)して、描画処理を行えるよう準備を行っている。 このコードでは、コンパイルされたシェーダーを変数shaderにセットし、その後の処理で使っている。

加えて、ここではシェーダーの処理とは別に、「バッチ(batch)」オブジェクトを以下のように定義している。 たくさんの描画データが存在するとき、そのうちのどれをどの順番で実際に描画すればよいかを指定するため、 Pygletではバッチとグループという機能が提供されている。 これらの詳細については、必要になった都度、補足することにしよう。

batch = pyglet.graphics.Batch()
頂点リストの生成

意外かもしれないが、OpenGLを使って描画することができる図形は、基本的に点と線分と三角形だけである。 複雑な図形はそれらを組み合わせて表現することになる。

vlist = shader.vertex_list(3, GL_TRIANGLES, batch=batch)

は、定義済のシェーダーに、頂点数3の三角形(GL_TRIANGLES)の頂点データを受け渡すための指定である。 そして、実際の描画はbatchという変数で指定される「バッチ」の中で処理される。

三角形の頂点は3つに決まっているのに、わざわざ3と指定するのは冗長に思えるかもしれないが、OpenGLでは、頂点座標の リストが与えられた際に、GL_TRIANGLESの場合、それを3つずつ区切りながら、複数の三角形を一気に描画することもできるので、 この例では、3のところが6や9であっても構わないわけだ。

基本的な図形(グラフィック・プリミティブ)としては、GL_TRIANGLESの他に、 GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_TRIANGLE_STRIPが指定できる。 一方で、PygletではGL_POLYGON, GL_LINE_LOOP, GL_TRIANGLE_FANはサポートされていないので注意が必要である。

描画用イベントハンドラーの定義

on_draw()関数に新たに加わった項目を順にみていこう:

【ビューポートの設定】

この例題では、ウィンドウのサイズは幅が400, 高さが300としているが、そのうちのどの部分を実際に描画領域にするかを設定できる。 そして、この描画領域のことをビューポートと呼ぶ。

ウィンドウの左下を(0,0)として、ウィンドウの中の(100,100)から、幅200, 高さ100の領域を描画用に設定する場合、 ビューポートの設定は以下のようになる。

window.viewport = (100, 100, 200, 100)

【プロジェクション行列とビュー変換行列の設定】

頂点シェーダーで処理するプロジェクション行列(頂点シェーダーで定義されている mat4 projection;)、 および、ビュー変換行列(頂点シェーダーで定義されている mat4 view;)に、それぞれ単位行列を設定する。

window.projection = Mat4()
window.view = Mat4()

によって設定している。ここでMat4は4行4列の行列を表すクラスで、 Mat4()のようにパラメータ無しでコンストラクターを呼び出すと、は4行4列の単位行列となる。

【モデル変換行列の設定】

頂点シェーダーで処理するモデル変換行列(頂点シェーダーで定義されている uniform mat4 model;)を 単位行列に設定する。

shader['model'] = Mat4()

この例のように、シェーダーのuniform変数(CPUとのデータのやり取りに使うための変数)は、

シェーダープログラム名['uniform変数名']

によってアクセスすることができる。

【頂点座標の設定】

すでに定義済みに頂点リストvlistに三角形の頂点座標を設定する。 座標は1次元的なリストとして与え、

vlist.position = (-0.9, -0.9, 0.9, -0.9, 0.0, 0.9)

は(-0.9, -0.9), (0.9, -0.9), (0.0, 0.9)の3つの頂点座標と解釈される。 これは、頂点シェーダーのin vec2 position;の箇所、すなわち、 positionという2次元ベクトルをCPUから受け渡すという宣言と対応している。

【頂点の色の設定】

OpenGLでは、各頂点に色の情報を指定することができる。この例では、頂点リストvlist

vlist.colors = (1.0, 0.0, 0.0, 1.0,  0.0, 1.0, 0.0, 1.0,  0.0, 0.0, 1.0, 1.0)

で色を指定している。各頂点の色は、RGBとアルファの4つの実数(0から1の範囲)で指定し、 この場合は、最初の頂点は(1.0, 0.0, 0.0, 1.0), 次が (0.0, 1.0, 0.0, 1.0), 最後が(0.0, 0.0, 1.0, 1.0)となる。 これは、頂点シェーダーのin vec4 colors;の箇所、すなわち、 colorsという4次元ベクトルをCPUから受け渡すという宣言と対応している。

OpenGLは、線分や面の色は、各頂点に指定された色を使って自動的に補完される。 したがって、線分や面を全く同じ色で表示したい場合は、各頂点に同じ色を指定しておく必要がある。

【描画の実行】

頂点リストvlistでバッチが指定されているので、

batch.draw()

によって、そのバッチに対応する頂点データが処理・描画される。 この例では頂点リストはひとつした定義していないが、複数の頂点リストを作成し、同じバッチを割り当てることによって、 バッチ名.draw()で一気に描画処理を行うことが可能となる。

【イベントループの開始】

prog-2-1.pyと全く同様である。

2.3 2次元描画での座標変換

プロジェクション変換

OpenGLは3次元的なグラフィック処理を行うための仕組みであるから、2次元的な図形を扱っている場合でも、内部では3次元的な処理が行われている。 ここでは、二次元図形がx-y平面上(すなわち$z=0$であるような平面)に配置されているものとして、考えることにしよう。 その意味で、OpenGLではz軸は「高さ」ではなくて「奥行き」や「深さ」と捉えたほうがわかりやすいだろう。

OpenGLのシステムでは、ウィンドウの中心が座標(0,0,0), ウィンドウの左下が(-1,-1,0), ウィンドウの右上が(1,1,0)、すなわち x軸方向に-1から+1まで、y軸方向にも-1から1までの矩形にスクリーンが重ねられているとして、描画が行われる。 であるから、ウィンドウの縦横比が1でないと図形が歪んで表示されてしまうし、様々な位置やサイズの図形を描画するには、このままでは具合が悪い。

そこで、図形の配置される座標を変換して、ウィンドウのスクリーンに「投映(projection)」するため、プロジェクション行列(projection matrix)を使って計算を行う。 プロジェクション行列は4行4列で構成され、回転や移動、変形を含む様々な変換が可能であるが、詳しいことは3次元グラフィックスの項目で改めて取り上げることにしたい。

ここでは、プロジェクション行列を計算してくれるPygletの関数

Mat4.orthogonal_projection(左のx座標, 右のx座標, 下のy座標, 上のy座標, 手前のz座標, 奥手のz座標)

を使ってスクリーンに投映する範囲を設定する方法を紹介しておく。例えば、on_draw()関数のプロジェクション行列の設定の箇所を

window.projection = Mat4.orthogonal_projection(-4, 4, -3, 3, -100, 100)

と変更すれば、三角形は小さく、かつ歪みが取れるはずである。

モデル変換

頂点リストを使って作成し配置した図形全体を(頂点座標の指定はそのままに)回転したり移動するには、モデル変換行列を設定すれば良い。

まず、ベクトル操作を行うために、冒頭でVec3モジュールを新たにインポートしておく

from pyglet.math import Mat4,Vec3

その上で、on_draw()関数の中でshader['model']にモデル変換行列を設定すればよい。 回転と移動、スケール変換を表すための行列はそれぞれ、

Mat4.from_rotation(ラジアン単位の回転角, Vec3(0,0,1))

Mat4.from_translation(Vec3(x移動量, y移動量, 0))

Mat4.from_scale(Vec3(x方向拡大率, y方向拡大率, 1))

として用意されているので、例えば、x方向に1だけ移動した後に、90度反時計方向に回転する変換は

shader['model'] = Mat4.from_rotation(3.1415/2, Vec3(0,0,1)) @ Mat4.from_translation(Vec3(1, 0, 0))

となる。 ここで、@は行列同士の積(NumPyと同様)を表す。 このとき、行列をかける順序が重要である。「先」に行う操作が右側、「後」が左側と覚えておこう。 変換行列$A$と$B$があったとき、ベクトル $\boldsymbol{x}$ をこれらに作用させる際に、$A \, B \, \boldsymbol{x}$とすれば「$B$が先で$A$が後」になることと対応している。


3. アニメーション表示とイベント処理を実装する

3.1 アニメーション

Pygletには、一定の時間間隔で指定した関数を呼び出す機能がある。 これを使って、図形の頂点座標やモデル変換行列を少しずつ変えるようにすれば、簡単なアニメーションが可能となる。

そこで、prog-2-2.pyに少し手を加え、周りを三角形を回転させるアニメーションを表示させるコードの例が以下である。

prog-3-1.py

import pyglet
from pyglet.gl import *
from pyglet.math import Mat4,Vec3

###
### シェダーのソースコードから頂点リストの生成のところまでは、prog-2-2.pyと同じ
###

rot_angle = 0.0

# 描画用イベントハンドラーの定義
@window.event
def on_draw():
    window.clear()
    window.viewport = (0, 0, 400, 300)
    window.projection = Mat4.orthogonal_projection(-4, 4, -3, 3, -100, 100)
    window.view = Mat4()
    shader['model'] = Mat4.from_rotation(rot_angle,Vec3(0,0,1)) @ Mat4.from_translation(Vec3(2,0,0))
    vlist.position = (-0.9, -0.9, 0.9, -0.9, 0.0, 0.9)
    vlist.colors = (1.0, 0.0, 0.0, 1.0,  0.0, 1.0, 0.0, 1.0,  0.0, 0.0, 1.0, 1.0)
    batch.draw()

# 時間毎の更新
def update(dt):
    global rot_angle
    rot_angle += 3.1415/30

# タイマー設定
pyglet.clock.schedule_interval(update, 1/30)

# イベントループ開始
pyglet.app.run()

prog-3-1.pyの実行中のスナップショット

prog-2-2.pyからの変更・追加箇所は以下の通りである。

冒頭でVec3もインポートしておく。

モデル行列の回転角度(ラジアン)を表すための変数として、rot_angleを用意しておく。

pyglet.clock.schedule_interval(update, 1/30)によって、1/30秒の間隔で、関数update()が呼び出されるようになる。 関数には前の呼び出しからの時間間隔が渡される。

関数update()では、呼び出しの都度、rot_angleの値を更新する(3.1415/30だけ増やす)。

一方、描画用のハンドラーon_draw()の中では、rot_angleの値に応じて、モデル行列を

shader['model'] = Mat4.from_rotation(rot_angle,Vec3(0,0,1)) @ Mat4.from_translation(Vec3(2,0,0))

によって変更する。

3.2 ウィンドウのリサイズに対応する

ウィンドウを生成する際に、resizable=Trueを指定すると、マウスでサイズが変更可能になる。 その場合、描画できる範囲や縦横比が変わってしまうので、正常に表示できるようにプロジェクション行列等を調整する必要がある。

prog-3-1.pyをもとに、画面サイズ変更に対応させたコードの例が以下である。

prog-3-2.py


###
### 前半部分は prog-3-1.pyと全く同様
###

# ウィンドウの生成
window = pyglet.window.Window(width=400,height=300,resizable=True)
glClearColor(0.3, 0.3, 0.5, 1.0)

# シェダーのコンパイル
vert_shader = pyglet.graphics.shader.Shader(vertex_source, 'vertex')
frag_shader = pyglet.graphics.shader.Shader(fragment_source, 'fragment')
shader = pyglet.graphics.shader.ShaderProgram(vert_shader, frag_shader)
batch = pyglet.graphics.Batch()

# 頂点リストの生成
vlist = shader.vertex_list(3, GL_TRIANGLES, batch=batch)

# リサイズ用イベントハンドラーの定義
@window.event
def on_resize(width,height):
    window.viewport = (0, 0, width, height)
    ratio = height/width
    window.projection = Mat4.orthogonal_projection(-4, 4, -4*ratio, 4*ratio, -100, 100)    
    window.view = Mat4()
    return pyglet.event.EVENT_HANDLED

rot_angle = 0.0

# 描画用イベントハンドラーの定義
@window.event
def on_draw():
    window.clear()
    shader['model'] = Mat4.from_rotation(rot_angle,Vec3(0,0,1)) @ Mat4.from_translation(Vec3(2,0,0))
    vlist.position = (-0.9, -0.9, 0.9, -0.9, 0.0, 0.9)
    vlist.colors = (1.0, 0.0, 0.0, 1.0,  0.0, 1.0, 0.0, 1.0,  0.0, 0.0, 1.0, 1.0)
    batch.draw()

# 時間毎の更新
def update(dt):
    global rot_angle
    rot_angle += 3.1415/30
    
# タイマー設定
pyglet.clock.schedule_interval(update, 1/30)

# イベントループ開始
pyglet.app.run()

新たな変更箇所は多く無いが、ウィンドウ作成の際に、resizable=Trueオプションを追加し

window = pyglet.window.Window(width=400,height=300,resizable=True)

とすることで、ウィンドウのりサイズボタンが有効になる。実際にサイズが変更されるとイベントが発生し、新たに追加したハンドラー

@window.event
def on_resize(width,height):
    window.viewport = (0, 0, width, height)
    ratio = height/width
    window.projection = Mat4.orthogonal_projection(-4, 4, -4*ratio, 4*ratio, -100, 100)    
    window.view = Mat4()
    return pyglet.event.EVENT_HANDLED

が呼び出される。ハンドラーには、変更後のウィンドウの幅と高さが渡されるので、縦横比を計算して、プロジェクション行列を調整するようにした。 ここでは、x方向の範囲は-4から4までの固定とし、縦方向を、図形の縦横比が正しくなるように設定している。 なお、ハンドラーの最後は必ずreturn pyglet.event.EVENT_HANDLEDで結果を返すしておく必要がある。

プロジェクション行列等の設定をon_resize()に移動したことから、on_draw()から当該の設定は削除してある。

3.3 マウスのホィールで図形を回転

今度は、タイマーによるアニメーションではなくて、マウスのホィールで三角形を回すようにしてみる。 そのためには、マウスのスクロール状態が変化したイベントを捕捉して、処理するハンドラー(on_mouse_scroll())を用意すればよい。

prog-3-3.py


###
### これより上は、prog-3-2.pyと同じ
###

rot_angle = 0.0

# 描画用イベントハンドラーの定義
@window.event
def on_draw():
    window.clear()
    shader['model'] = Mat4.from_rotation(rot_angle,Vec3(0,0,1)) @ Mat4.from_translation(Vec3(2,0,0))
    vlist.position = (-0.9, -0.9, 0.9, -0.9, 0.0, 0.9)
    vlist.colors = (1.0, 0.0, 0.0, 1.0,  0.0, 1.0, 0.0, 1.0,  0.0, 0.0, 1.0, 1.0)
    batch.draw()

@window.event
def on_mouse_scroll(x, y, scroll_x, scroll_y):
    global rot_angle
    rot_angle += scroll_y * 3.1415/30

# イベントループ開始
pyglet.app.run()

prog-3-2.pyからの変更箇所は、タイマー設定 pyglet.clock.schedule_interval(update, 1/30)、およびupdate()関数を削除し、 代わりに、イベントハンドラー

@window.event
def on_mouse_scroll(x, y, scroll_x, scroll_y):
    global rot_angle
    rot_angle += scroll_y * 3.1415/30

を追加したのみである。


4. pygletの組み込みオブジェクトを使って図形を描画する

4.1 Shapeオブジェクトの使用例

ここまで読まれた方は、単純な三角形の描画だけでもとても面倒なことに驚かれたかもしれない。 実のところ、いくつかの基本的な図形、文字、それから画像データを表示する機能はpygletに内蔵されていて、 通常の2次元的な描画は、それらを使うのがはるかに簡単である。

なんだかハシゴを外されたように感じるかもしれないが、内部ではシェーダーを使ったOpenGLの描画が行われていることに違いはなく、これまでに説明したOpenGLを使った方法と併用することができる。

以下はいくつかの図形オブジェクトを表示した上で、マウスでクリックした点に「月」の画像を移動し、ホィールで全体を回転させるコードの例である。

プログラム中で月の画像データmoon.pngを読み込むようになっているので、プログラムと同じディレクトリにあらかじめダウンロードしておくこと。

prog-4-1.py

画像ファイル: moon.png


import pyglet
from pyglet import shapes, text
from pyglet.math import Mat4,Vec3

# ウィンドウの生成
window = pyglet.window.Window(width=1280,height=720,resizable=True)
pyglet.gl.glClearColor(0.3, 0.3, 0.5, 1.0)

# バッチの生成
batch = pyglet.graphics.Batch()

# 図形オブジェクトの生成
image = pyglet.image.load('moon.png')
sprite = pyglet.sprite.Sprite(img=image, x=150, y=130,  batch=batch)

label = text.Label('Pygletの2次元図形', font_name='Arial', font_size=36, color=(10,200,250,255), \
                   x=-400, y=200, anchor_x='left', anchor_y='center', batch=batch)

bezier = shapes.BezierCurve((-450,100),(100,-100),(0,-300),(200,-500),(400,200),thickness=5, \
                   color=(100,200,200,200),batch=batch)

circle = shapes.Circle(-300, -100, 100, color=(255, 200, 40), batch=batch)

rectangle = shapes.Rectangle(-150, -200, 100, 250, color=(120, 200, 150), batch=batch)

box = shapes.Box(x=100, y=-150, width=200, height=200, thickness=3, color=(200, 200, 255), batch=batch)

vlist = [(100,-200),(0,-100),(0,0),(100,100),(300,100),(400,0),(400,-100),(300,-200)]
polygon = shapes.Polygon(*vlist, color=(200,10,210, 100), batch=batch)

star = shapes.Star(x=410, y=240, outer_radius=30, inner_radius=5, num_spikes=8, color=(240, 240, 255), \
                   batch=batch)


# リサイズ用イベントハンドラーの定義
@window.event
def on_resize(width,height):
    window.viewport = (0, 0, width, height)
    ratio = height/width
    window.projection = Mat4.orthogonal_projection(-500, 500, -500*ratio, 500*ratio, -100, 100)    
    return pyglet.event.EVENT_HANDLED

rot_angle = 0.0

# 描画用イベントハンドラーの定義
@window.event
def on_draw():
    window.clear()
    window.view = Mat4.from_rotation(rot_angle, Vec3(0,0,1))
    batch.draw()

@window.event
def on_mouse_scroll(x, y, scroll_x, scroll_y):
    global rot_angle
    rot_angle += scroll_y * 3.1415/60

@window.event
def on_mouse_press(x,y,button,modifier):
    x -= image.width/2
    y -= image.height/2
    x2 = (x-window.width/2)*1000/window.width
    y2 = (y-window.height/2)*1000/window.width
    sprite.x = x2
    sprite.y = y2

# イベントループ開始
pyglet.app.run()

prog-4-1.pyの実行画面

pygletのShapeオブジェクトを使って色の指定をする際は、OpenGL流儀の0から1の実数ではなく、0から255の整数値を用いる必要がある。

この例では、画像データ(月の写真)を読み込んで、画面に貼り付けている(スプライトと呼ばれる)。 この機能を使うと、昔ながらのアーケードゲームのように、アイテムやキャラクター等の画像を用意しておいて、画面の好きな位置に配置したり、動かしたりすることもできる。

マウス操作のイベントハンドラーに渡される座標は、左下を(0,0)、右上が(ウィンドウの幅-1,ウィンドウの高さ-1)となるような値となっているため、 プロジェクション行列で設定した座標に変換する必要がある。 on_mouse_press()関数の中の

x -= image.width/2
y -= image.height/2
x2 = (x-window.width/2)*1000/window.width
y2 = (y-window.height/2)*1000/window.width

の箇所でこの変換を行っている。この例では、ウィンドウの中心を原点(0,0)に設定しており、ウィンドウの幅が長さ1000に相当し、 高さは横幅を基準としてウィンドウの縦横比(アスペクト比)で調整されている。 また、クリックした点を画像の中央付近に揃えるため、取得したスクリーン座標(x,y)をあらかじめ画像の幅(image.width)と高さ(image.height)の半分だけオフセットさせている。

4.2 複雑な図形の描画

このページの最後に、複雑(?)な図形の例として、シェルピンスキー・カーペットと呼ばれるフラクタルな図形を描くコードの例を示す。 マウスでドラッグして注目点を変えたり、ホィールでズーミングできるようになっているので、細部の様子を観察することができる。

一旦頂点のデータを作成しておけば、拡大や縮小の計算はGPUが処理するので、頂点数の多い図形であっても高速な処理が可能である。

prog-4-2.py

import pyglet
from pyglet import shapes, text
from pyglet.math import Mat4,Vec3
import math

# ウィンドウの生成
# config = pyglet.gl.Config(sample_buffers = 1,samples = 4,doubel_buffer=True)
window = pyglet.window.Window(width=1280,height=720,resizable=True)
# pyglet.gl.glEnable(pyglet.gl.GL_LINE_SMOOTH)
pyglet.gl.glClearColor(0.3, 0.3, 0.5, 1.0)

# バッチの生成
batch = pyglet.graphics.Batch()

# Sierpinskiカーペットの生成
triangles = [ ]
def sierpinski(level,x,y):
    ell = (1/2)**(level-1)
    if level<=7:
        sierpinski(level+1,x-(ell/4), y-ell*math.sqrt(3)/12)
        sierpinski(level+1,x+(ell/4), y-ell*math.sqrt(3)/12)
        sierpinski(level+1,x, y+ell*math.sqrt(3)/6)
    else:
        vertices = [x-ell*1/2, y-ell*math.sqrt(3)/6, x+ell*1/2, y - ell*math.sqrt(3)/6, x, y+ell*math.sqrt(3)/3]
        triangle = shapes.Triangle(*vertices, color=(255, 200, 40), batch=batch)
        triangles.append(triangle)

# リサイズ用イベントハンドラーの定義
@window.event
def on_resize(width,height):
    window.viewport = (0, 0, width, height)
    ratio = width/height
    window.projection = Mat4.orthogonal_projection(-1*ratio, 1*ratio, -1, 1, -100, 100)    
    return pyglet.event.EVENT_HANDLED

zoom = 1.0
pos_dx = 0
pos_dy = 0

# 描画用イベントハンドラーの定義
@window.event
def on_draw():
    window.clear()
    window.view = Mat4.from_translation(Vec3(pos_dx, pos_dy, 0)) @ Mat4.from_scale(Vec3(zoom,zoom,1.0))
    batch.draw()

@window.event
def on_mouse_drag(x, y, dx, dy, buttons, modifiers):
    global pos_dx, pos_dy
    pos_dx += 2*dx/window.height
    pos_dy += 2*dy/window.height
    
@window.event
def on_mouse_scroll(x, y, scroll_x, scroll_y):
    global zoom
    zoom *= math.exp(scroll_y/100)

sierpinski(0,0,0)

# イベントループ開始
pyglet.app.run()

prog-4-2.pyの実行画面

このコードでは、イベントハンドラーon_mouse_drag()でマウスのドラッグ量を取得し、ビュー行列の設定に反映させている。 on_resize()関数の中のプロジェクションの設定で、ウィンドウの下端のy座標を-1、上端を+1に設定してるので、 ウィンドウの高さが長さ2に対応するようなスケール変換を行っていることになる。そこで、

pos_dx += 2*dx/window.height
pos_dy += 2*dy/window.height

のようにして、画素単位のドラッグの量を、ビュー座標での変位量に変換するようにしている。 この例のように、マウス関係のイベントハンドラーにはウィンドウ上の座標での値が渡されるので、適宜、変換が必要となる。

次のパートへ