OpenGL を Python3 から利用するための環境設定および簡単なサンプルコード (Linux)
[履歴] [最終更新] (2018/09/10 00:27:34)
1
作品
409
技術情報
最近の投稿
ここは
趣味の電子工作を楽しむ人のためのハードウェア情報共有サイト

技術情報や作品の投稿機能、リアルタイム遠隔操作 API をご利用いただけます。
新着作品

概要

コンピュータグラフィックスのレンダリングライブラリの一つである OpenGL を Python3 から利用するための Linux 環境を準備して、設定例およびサンプルコードを記載します。特にここでは Debian9 を利用します。

ModernGL インストール

Python バインディングのうち、ここでは ModernGL を介して OpenGL を利用することにします。ただし、必要となる Python は 3 系です。

Python3 および pip、ipythonnumpy、matplotlib、pil

sudo apt install python3
sudo apt install python3-pip
sudo apt install python3-ipython
sudo apt install python3-numpy
sudo apt install python3-matplotlib
sudo apt install python3-pil

ModernGL

以下のいずれかでインストールできます。

/usr/bin/python3 -m pip install ModernGL
/usr/bin/pip3 install ModernGL

動作確認

IPython 起動

/usr/bin/python3 -m IPython

以下の内容を実行します。

import moderngl
ctx = moderngl.create_standalone_context()
buf = ctx.buffer(b'Hello World!')  # allocated on the GPU
buf.read()

出力例

b'Hello World!'

3D レンダリング Hello World

3D モデルから 2D 画像を生成する 3D レンダリングの簡単な例を記載します。IPython 等で以下の内容を実行します。

DISPLAY=:0 /usr/bin/python3 -m IPython

OpenGL ではまずコンテキストが必要になります

import moderngl
ctx = moderngl.create_standalone_context()

下記レンダリングパイプラインにおいて、頂点シェーダーへの入力となる、3 つの頂点の情報を準備します。

import numpy as np
vertices = np.array([
     0.5,  0.5, 0.0,
    -0.5,  0.5, 0.0,
    -0.5, -0.5, 0.0
])

これらをバイトに変換して GPU 上にデータを格納し、Buffer (Vertex Buffer Object) を作成します。

vbo = ctx.buffer(vertices.astype('float32').tobytes())

ModernGL をインストールすると付属のバージョンの OpenGL がインストールされます。OpenGL には OpenGL Shading Language (GLSL) とよばれる、シェーダーを記述するための言語の処理系が実装されています。GLSL を用いて、以下の二つのシェーダーを作成します。これらは一連のレンダリングパイプラインで利用されます。

  • Vertex Shader (頂点シェーダー)
    • 入力された、3D モデルにおけるローカル座標系の頂点情報をワールド座標系に変換して、更にあるカメラの視点から見たときの座標系に変換して 2 次元情報に射影します。
  • Fragment Shader (ピクセルシェーダー)
    • 前工程のレンダリングパイプラインで生成されたフラグメントに色情報等を設定します。

OpenGL 3.3 における GLSL version 330 を利用しています。簡単のため、入力された三次元頂点情報 vec3 をそのまま加工せずに返しています。その際、同次座標系で返す必要があるため 1.0 を追加して vec4 にしています。また、各フラグメントの色も簡単のため RGBA で青色を返しています。

prog = ctx.program(
    vertex_shader='''
        #version 330
        in vec3 in_vert;
        void main() {
            gl_Position = vec4(in_vert, 1.0);
        }
    ''',
    fragment_shader='''
        #version 330
        out vec4 f_color;
        void main() {
            f_color = vec4(0.0, 0.0, 1.0, 1.0);
        }
    '''
)

頂点情報 Buffer をシェーダー prog に変数名を指定して渡します。結果として Vertex Array Object (vao) が得られます。

vao = ctx.simple_vertex_array(prog, vbo, 'in_vert')

レンダリング先となる 2D フレーム Frame Buffer Object を用意します。500x500 ピクセルとしてみます。

fbo = ctx.simple_framebuffer((500, 500))
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 1.0)

レンダリングを実行します。結果が fbo に格納されます。

vao.render()

結果は RAW 画像であるため、MatplotlibOpenCV からは直接読み込むことが困難です。pillow(PIL) の Image.frombytes を利用して NumPy の ndarray に変換することができます。

from PIL import Image
myimg = Image.frombytes('RGB', fbo.size, fbo.read(), 'raw', 'RGB', 0, -1)

以下のようにして表示できます。

from matplotlib import pyplot as plt
plt.imshow(myimg)
plt.show()

Uploaded Image

頂点毎に異なる色を設定

先程の Hello world プログラムでは各頂点をすべて青色にしました。色を変えるために頂点シェーダーへの引数を増やしてみます。

import moderngl
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt

# OpenGL コンテキスト
ctx = moderngl.create_standalone_context()

# 頂点情報 (x,y,z,r,g,b)
vertices = np.array([
     0.5,  0.5, 0.0, 1.0, 0.0, 0.0,
    -0.5,  0.5, 0.0, 0.0, 1.0, 0.0,
    -0.5, -0.5, 0.0, 0.0, 0.0, 1.0
])
vbo = ctx.buffer(vertices.astype('float32').tobytes())

# シェーダープログラム
prog = ctx.program(
    vertex_shader='''
        #version 330
        in vec3 in_vert;
        in vec3 in_color;
        out vec3 v_color;

        void main() {
            v_color = in_color;
            gl_Position = vec4(in_vert, 1.0);
        }
    ''',
    fragment_shader='''
        #version 330
        in vec3 v_color;
        out vec4 f_color;
        void main() {
            f_color = vec4(v_color, 1.0);
        }
    '''
)

# 色情報の引数を増やしています
vao = ctx.simple_vertex_array(prog, vbo, 'in_vert', 'in_color')

# 2D フレームの作成
fbo = ctx.simple_framebuffer((500, 500))
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 1.0)

# レンダリングの実行
vao.render()

# RAW 画像から ndarray に変換して描画
myimg = Image.frombytes('RGB', fbo.size, fbo.read(), 'raw', 'RGB', 0, -1)
plt.imshow(myimg)
plt.show()

Uploaded Image

二次元 XY 平面内での回転

GPU 内で利用する uniform 定数を宣言しています。

import moderngl
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt

# OpenGL コンテキスト
ctx = moderngl.create_standalone_context()

# 頂点情報 (x,y,z,r,g,b)
vertices = np.array([
     0.5,  0.5, 0.0, 1.0, 0.0, 0.0,
    -0.5,  0.5, 0.0, 0.0, 1.0, 0.0,
    -0.5, -0.5, 0.0, 0.0, 0.0, 1.0
])
vbo = ctx.buffer(vertices.astype('float32').tobytes())

# シェーダープログラム
prog = ctx.program(
    vertex_shader='''
        #version 330
        in vec3 in_vert;
        in vec3 in_color;
        out vec3 v_color;

        // uniform によって GPU 内で利用する定数を宣言できます。
        // 引数ではなく属性値として CPU から値を設定します。
        uniform vec3 scale;
        uniform float rotation;

        void main() {
            // 二次元の回転行列
            // mat3 の仕様上、行と列を転置したものを設定します。
            mat3 rot = mat3(
                cos(rotation), sin(rotation), 0.0,
                -sin(rotation), cos(rotation), 0.0,
                0.0, 0.0, 1.0
            );

            v_color = in_color;
            gl_Position = vec4((rot * in_vert) * scale, 1.0);
        }
    ''',
    fragment_shader='''
        #version 330
        in vec3 v_color;
        out vec4 f_color;
        void main() {
            f_color = vec4(v_color, 1.0);
        }
    '''
)

# prog 内で宣言した GPU 用の定数の値を設定します。
prog['scale'].value = (2.0, 2.0, 1.0)
prog['rotation'].value = np.deg2rad(90)

# 頂点情報 (位置、色) をシェーダーに渡します。
vao = ctx.simple_vertex_array(prog, vbo, 'in_vert', 'in_color')

# 2D フレームの作成
fbo = ctx.simple_framebuffer((500, 500))
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 1.0)

# レンダリングの実行
vao.render()

# RAW 画像から ndarray に変換して描画
myimg = Image.frombytes('RGB', fbo.size, fbo.read(), 'raw', 'RGB', 0, -1)
plt.imshow(myimg)
plt.show()

反時計回りに 90 度回転させてから 2 倍に拡大しています。mat3 は column-major order で値を指定する必要があることに注意します。

Uploaded Image

オブジェクトファイルの利用

3D オブジェクトはメッシュによって表現できます。メッシュは頂点やフェイス等から成ります。通常フェイスには三角形を利用します。メッシュの情報を格納するファイルフォーマットとしては OBJ がよく用いられます。以下は簡単な例です。

sample.obj

v   0.5  0.5  0.0
v  -0.5  0.5  0.0
v  -0.5 -0.5  0.0
v   0.5 -0.5  0.0

f 1 2 3
f 3 4 1

v は頂点のローカル座標です。f では三角形フェイスで利用する頂点 v のインデックスを三つ指定します。今回のメッシュでは三角形フェイスが二つで正方形を表現しています。OBJ ファイルを読み込むために何らかのモジュールをインストールすると便利です。

/usr/bin/python3 -m pip install objloader

以下のようなレンダリング結果が得られます。

Uploaded Image

import moderngl
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from objloader import Obj

# OpenGL コンテキスト
ctx = moderngl.create_standalone_context()

# 頂点情報 (x,y,z)
obj = Obj.open('./sample.obj')
vbo = ctx.buffer(obj.pack('vx vy vz'))

# シェーダープログラム
prog = ctx.program(
    vertex_shader='''
        #version 330
        in vec3 in_vert;
        void main() {
            gl_Position = vec4(in_vert, 1.0);
        }
    ''',
    fragment_shader='''
        #version 330
        out vec4 f_color;
        void main() {
            f_color = vec4(0.0, 0.0, 1.0, 1.0);
        }
    '''
)

# Vertex Array Object の作成
vao = ctx.simple_vertex_array(prog, vbo, 'in_vert')

# 2D フレームの作成
fbo = ctx.simple_framebuffer((500, 500))
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 1.0)

# レンダリングの実行
vao.render()

# RAW 画像から ndarray に変換して描画
myimg = Image.frombytes('RGB', fbo.size, fbo.read(), 'raw', 'RGB', 0, -1)
plt.imshow(myimg)
plt.show()

オブジェクトへのテクスチャの貼り付け

sample.obj

v   0.5  0.5  0.0
v  -0.5  0.5  0.0
v  -0.5 -0.5  0.0
v   0.5 -0.5  0.0

vt 0.0 0.0
vt 1.0 0.0
vt 1.0 1.0
vt 0.0 1.0

f 1/3 2/4 3/1
f 3/1 4/2 1/3

vt はテクスチャ画像における座標です。フェイスでは / 区切りでどのテクスチャ座標を頂点座標と対応させるか指定します。

import moderngl
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from objloader import Obj

# OpenGL コンテキスト
ctx = moderngl.create_standalone_context()

# 頂点座標 (x,y,z)、テクスチャ座標 (x,y)
obj = Obj.open('./sample.obj')
vbo = ctx.buffer(obj.pack('vx vy vz tx ty'))

# シェーダープログラム
prog = ctx.program(
    vertex_shader='''
        #version 330
        in vec3 in_vert;
        in vec2 in_text;
        out vec2 v_text;
        void main() {
            v_text = in_text;
            gl_Position = vec4(in_vert, 1.0);
        }
    ''',
    fragment_shader='''
        #version 330
        uniform sampler2D Texture;
        in vec2 v_text;
        out vec4 f_color;
        void main() {
            f_color = vec4(texture(Texture, v_text).rgb, 1.0);
        }
    ''',
)

# Vertex Array Object の作成
vao = ctx.simple_vertex_array(prog, vbo, 'in_vert', 'in_text')

# テクスチャ画像の読み込み
textureimg = Image.open('./myimage.png').transpose(Image.FLIP_TOP_BOTTOM).convert('RGB')
texture = ctx.texture(textureimg.size, 3, textureimg.tobytes())
texture.build_mipmaps()
texture.use()

# 2D フレームの作成
fbo = ctx.simple_framebuffer((500, 500))
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 1.0)

# レンダリングの実行
vao.render()

# RAW 画像から ndarray に変換して描画
myimg = Image.frombytes('RGB', fbo.size, fbo.read(), 'raw', 'RGB', 0, -1)
plt.imshow(myimg)
plt.show()

Uploaded Image

オブジェクトの回転

XY 平面内の回転等であれば簡単ですが、任意軸回りの回転を行うためには四元数 Quaternions が利用できることが知られています。

sample.obj

v   0.5  0.5  0.0
v  -0.5  0.5  0.0
v  -0.5 -0.5  0.0
v   0.5 -0.5  0.0

vt 0.0 0.0
vt 1.0 0.0
vt 1.0 1.0
vt 0.0 1.0

f 1/3 2/4 3/1
f 3/1 4/2 1/3

単位方向ベクトル (1/sqrt(2), 1/sqrt(2), 0) 回りに反時計回りに 60 度回転させる例は以下のようになります。OpenGL 側ではなく Python スクリプト内で処理していますが、内容としては OpenGL の glRotatef 等を利用した場合と同様です。

import moderngl
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from objloader import Obj

# OpenGL コンテキスト
ctx = moderngl.create_standalone_context()

# 頂点座標 (x,y,z)、テクスチャ座標 (x,y)
obj = Obj.open('./sample.obj')

# 単位方向ベクトル、回転させる角度
v = np.array([1/np.sqrt(2), 1/np.sqrt(2), 0])
theta = np.deg2rad(60)

# Quaternion の計算
q = np.hstack((v * np.sin(theta/2), np.array([np.cos(theta/2)])))

# 長さは 1 です。
print(np.linalg.norm(q))

# 回転行列 3x3 を Quaternion から計算できます。
R = np.array([
    [1 - 2*(q[1]*q[1] + q[2]*q[2]), 2*(q[0]*q[1] - q[2]*q[3]),     2*(q[0]*q[2] + q[1]*q[3])],
    [2*(q[0]*q[1] + q[2]*q[3]),     1 - 2*(q[0]*q[0] + q[2]*q[2]), 2*(q[1]*q[2] - q[0]*q[3])],
    [2*(q[0]*q[2] - q[1]*q[3]),     2*(q[1]*q[2] + q[0]*q[3]),     1 - 2*(q[0]*q[0] + q[1]*q[1])]
])

# OBJ ファイルの読み込み結果を処理して頂点座標だけ抜き出します。回転行列を適用して再度結合します。
A,B,C = np.hsplit(obj.to_array(), 3)
vertices = np.hstack((R.dot(A.T).T, C[:,:2])).flatten()
vbo = ctx.buffer(vertices.astype('float32').tobytes())

# シェーダープログラム
prog = ctx.program(
    vertex_shader='''
        #version 330
        in vec3 in_vert;
        in vec2 in_text;
        out vec2 v_text;
        void main() {
            v_text = in_text;
            gl_Position = vec4(in_vert, 1.0);
        }
    ''',
    fragment_shader='''
        #version 330
        uniform sampler2D Texture;
        in vec2 v_text;
        out vec4 f_color;
        void main() {
            f_color = vec4(texture(Texture, v_text).rgb, 1.0);
        }
    ''',
)

# Vertex Array Object の作成
vao = ctx.simple_vertex_array(prog, vbo, 'in_vert', 'in_text')

# テクスチャ画像の読み込み
textureimg = Image.open('./myimage.png').transpose(Image.FLIP_TOP_BOTTOM).convert('RGB')
texture = ctx.texture(textureimg.size, 3, textureimg.tobytes())
texture.build_mipmaps()
texture.use()

# 2D フレームの作成
fbo = ctx.simple_framebuffer((500, 500))
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 1.0)

# レンダリングの実行
vao.render()

# RAW 画像から ndarray に変換して描画
myimg = Image.frombytes('RGB', fbo.size, fbo.read(), 'raw', 'RGB', 0, -1)
plt.imshow(myimg)
plt.show()

Uploaded Image

関連ページ
    概要 3D アプリケーション間でデータを交換するためのファイルフォーマットの一つに COLLADA (COLLAborative Design Activity) があります。コンピュータグラフィックスのレンダリングに必要な情報およびその他付随する情報を格納できます。COLLADA の仕様にしたがった XML スキーマファイルの拡張子は通常