モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

目次目次を開く/閉じる

OpenGL を Python3 から利用するための環境設定および簡単なサンプルコード (Linux)

モーダルを閉じる

ステッカーを選択してください

お支払い手続きへ
モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2018/09/07
最終更新最終更新
2024/02/17
記事区分記事区分
一般公開

目次

    インフラ構築と自動化が得意。TerraformとAnsibleでインフラを自動構築するお仕事が多め

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

    ModernGL インストール

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

    Python3 および pip、ipython]、numpy、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 (ピクセルシェーダー)
      • 前工程のレンダリングパイプラインで生成されたフラグメントに色情報等を設定します。

    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 画像であるため、Matplotlib や OpenCV からは直接読み込むことが困難です。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()
    

    頂点毎に異なる色を設定

    先程の 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()
    

    二次元 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 で値を指定する必要があることに注意します。

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

    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
    

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

    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()
    

    オブジェクトの回転

    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 から計算できます。
    # Quaternion の生成時に cos を第一要素にしている場合は添字に注意してください。
    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()
    

    Likeボタン(off)0
    詳細設定を開く/閉じる
    アカウント プロフィール画像

    インフラ構築と自動化が得意。TerraformとAnsibleでインフラを自動構築するお仕事が多め

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    >>さらに詳しくステッカーを贈る
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      ログインする

      関連記事