ライブラリを用いない OpenGL/EGL の使い方 (X11、Python)
[History] [Last Modified] (2019/05/06 03:57:43)
Recent posts
What is this site?
A platform for makers to share their knowledge.

Share your robots/products with others.
New robots/products

概要

コンピュータグラフィックスのレンダリングライブラリの一つ OpenGL はプラットフォームに依存しない仕様となっています。プラットフォームの一つに X11 があります。プラットフォームに依存する仕様は EGL (Embedded-System Graphics Library) にまとめられています。EGL は OpenGL とネイティブプラットフォームの間のインタフェースとして機能します。

OpenGL および EGL の実装の一つに Mesa 3D があります。その他に NVIDIA、AMD、Intel などの GPU ドライバの OpenGL/EGL 実装があります。

OpenGL を Python から利用するためのライブラリの一つに ModernGL があります。ここでは、OpenGL/EGL をライブラリを用いずに直接 Python から ctypes で利用する方法を記載します。実装としては Mesa をインストールすることにします。Debian9 の場合は以下のようにしてインストールできます。ウィンドウシステムとしては X11 を扱います。

sudo apt install libgl1-mesa-dev
sudo apt install libegl1-mesa-dev

/usr/lib/x86_64-linux-gnu/libGL.so
/usr/lib/x86_64-linux-gnu/libEGL.so

ドキュメント

ディスプレイ取得、初期化、コンフィグ選択、サーフィス作成、コンテキスト作成、カレント化 (EGL)

ipython

必要なライブラリの読み込み

ctypes で読み込む型はヘッダーファイルを利用して確認します。例えば egl.h によると EGLDisplayc_void_p です。

typedef void *EGLDisplay;

from ctypes import c_int
from ctypes import c_uint
from ctypes import c_float
from ctypes import c_void_p
from ctypes import POINTER
from ctypes import CDLL
from ctypes import CFUNCTYPE
from ctypes import byref
from ctypes.util import find_library
from functools import partial
from numpy import zeros
from numpy import uint8

libEGL.so の読み込み

eglLib = CDLL(find_library('EGL'))

必要な関数の読み込みを補助する関数を用意

ctypes では利用する関数の引数と返り値の型を設定する必要があります。argtypesrestype を指定する方法は以下の方法以外にもありますfunctoolspartial()load() の引数を一部固定できます。

def load(lib, name, restype, *args):
    return (CFUNCTYPE(restype, *args))((name, lib))

loadEgl = partial(load, eglLib)

ディスプレイの取得 eglGetDisplay

仕様によると eglGetDisplay の引数の型は EGLNativeDisplayType で返り値の型は EGLDisplay です。EGL_DEFAULT_DISPLAY を引数に指定して、DISPLAY 環境変数で指定されているものを利用するようにします。

EGLNativeDisplayType = c_void_p
EGLDisplay = c_void_p
EGL_DEFAULT_DISPLAY = EGLNativeDisplayType(0)

eglGetDisplay = loadEgl('eglGetDisplay', EGLDisplay, EGLNativeDisplayType)
display = eglGetDisplay(EGL_DEFAULT_DISPLAY)

if not display:
    raise Exception('no EGL display')

ディスプレイの初期化 eglInitialize

EGLBoolean = c_uint
EGLint = c_int

eglInitialize = loadEgl('eglInitialize', EGLBoolean, EGLDisplay , POINTER(EGLint), POINTER(EGLint))

major = EGLint(0)
minor = EGLint(0)
if not eglInitialize(display, byref(major), byref(minor)):
    raise Exception('cannot initialize EGL display')

初期化できない場合は適切な X11 を選択しているか確認します。例えば DISPLAY 0 の場合は以下のようになります。

DISPLAY=:0 ipython

初期化に成功している場合は EGL のバージョンが引数に指定した変数に格納されています。

In [1]: major
Out[1]: c_int(1)

In [2]: minor
Out[2]: c_int(4)

ただし、Mesa のバージョンと環境によっては以下のような警告が出ます。後に OpenGL で描画しても画像を取得できません。デフォルトのフレームバッファではなく、フレームバッファを新規に作成してから描画する必要があります

libEGL warning: DRI2: failed to authenticate

API のバインド eglBindAPI

更に OpenGL の API セットを選択します。既定値は EGL_OPENGL_ES_API です。

EGL_OPENGL_API = 0x30A2
EGLenum = c_uint

eglBindAPI = loadEgl('eglBindAPI', EGLBoolean, EGLenum)

if not eglBindAPI(EGLenum(EGL_OPENGL_API)):
    raise Exception('cannot bind API')

コンフィグの選択 eglChooseConfig

egl.h にある定数のうち必要なものを用意します。

EGL_BLUE_SIZE = 0x3022
EGL_GREEN_SIZE = 0x3023
EGL_RED_SIZE = 0x3024
EGL_DEPTH_SIZE = 0x3025
EGL_SURFACE_TYPE = 0x3033
EGL_NONE = 0x3038
EGL_PBUFFER_BIT = 0x01

EGLConfig = c_void_p

コンフィグ選択用の関数を読み込みます。

eglChooseConfig = loadEgl('eglChooseConfig', EGLBoolean, EGLDisplay, POINTER(EGLint), POINTER(EGLConfig), EGLint, POINTER(EGLint))

第二引数 POINTER(EGLint) を生成するための補助関数を定義します。

def makeCtypeArray(ctype, contents):
    arrayType = ctype * len(contents)
    return arrayType(*contents)

ディスプレイの属性とその値を交互に配列に記載していきます。最後は EGL_NONE で閉じます。

attribList = makeCtypeArray(EGLint, [
    EGL_BLUE_SIZE, 8,
    EGL_GREEN_SIZE, 8,
    EGL_RED_SIZE, 8,
    EGL_DEPTH_SIZE, 24,
    EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
    EGL_NONE])

コンフィグを選択します。

config = EGLConfig()
configSize = EGLint(1)
numConfig = EGLint(0)

if not eglChooseConfig(display, attribList, byref(config), configSize, byref(numConfig)):
    raise Exception('no suitable configs available on display')

サーフィスの作成 eglCreatePbufferSurface

Window 画面ではなくメモリ上の仮想的な空間に描画することをオフスクリーンレンダリングとよびます。OpenGL でオフスクリーンレンダリングを行うためには描画対象となるサーフィスを pBuffer (pixel buffer) で用意します。pBuffer ではなく Frame Buffer Object を利用することもできます。pBuffer サーフィスを作成するためには eglCreatePbufferSurface を利用します。

EGL_HEIGHT = 0x3056
EGL_WIDTH = 0x3057
EGLSurface = c_void_p

eglCreatePbufferSurface = loadEgl('eglCreatePbufferSurface', EGLSurface, EGLDisplay, EGLConfig, POINTER(EGLint))

surfaceAttribList = makeCtypeArray(EGLint, [
    EGL_WIDTH, 100,
    EGL_HEIGHT, 100,
    EGL_NONE])

surface = eglCreatePbufferSurface(display, config, surfaceAttribList)
if not surface:
    raise Exception('cannot create pbuffer surface')

コンテキストの作成 eglCreateContext

EGL_CONTEXT_CLIENT_VERSION = 0x3098
EGLContext = c_void_p
EGL_NO_CONTEXT = EGLContext(0)

eglCreateContext = loadEgl('eglCreateContext', EGLContext, EGLDisplay, EGLConfig, EGLContext, POINTER(EGLint))

contextAttribList = makeCtypeArray(EGLint, [EGL_NONE])

context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribList)
if not context:
    raise Exception('cannot create GL context')

作成したコンテキストとサーフィスのカレント化 eglMakeCurrent

一つのディスプレイに対して複数のサーフィスを作成できます。そのうちカレントとなるものを一つ選択します。OpenGL API を利用して描画する際の対象となります。

eglMakeCurrent = loadEgl('eglMakeCurrent', EGLBoolean, EGLDisplay, EGLSurface, EGLSurface, EGLContext)

if not eglMakeCurrent(display, surface, surface, context):
    raise Exception('cannot make GL context current')

描画 (OpenGL)

今回、ディスプレイのウィンドウと対応していない pBuffer をサーフィスとして利用しています。ModernGL を利用したときと同様に、pBuffer サーフィスにレンダリングした後に、サーフィスに描画されたデータを OpenGL の関数で読み出して画像として動作確認することにします。

libGL.so の読み込み

glLib = CDLL(find_library('GL'))

必要な関数の読み込みを補助する関数を用意

loadGl = partial(load, glLib)

エラーを取得する関数を読み込み

glGetError を読み込みます。glcorearb.h を参照して必要な定数を定義します。

GL_NO_ERROR = 0
GLenum = c_uint

glGetError = loadGl('glGetError', GLenum)

サーフィスへの描画

glClearColor で緑色を指定します。

GLfloat = c_float
glClearColor = loadGl('glClearColor', None, GLfloat, GLfloat, GLfloat, GLfloat)
glClearColor(GLfloat(0.0), GLfloat(1.0), GLfloat(0.0), GLfloat(1.0))
if not glGetError() == GL_NO_ERROR:
    raise Exception('glClearColor failed')

フレームバッファのカラーバッファを glClear で塗り潰します。

GL_COLOR_BUFFER_BIT = 0x00004000
GL_DEPTH_BUFFER_BIT = 0x00000100

GLbitfield = c_uint

glClear = loadGl('glClear', None, GLbitfield)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))

if not glGetError() == GL_NO_ERROR:
    raise Exception('glClear failed')

サーフィスからのデータの読み出し

OpenGL には複数のバッファが登場します。描画する対象となるバッファをフレームバッファとよびます。フレームバッファはカラーバッファ、デプスバッファ、ステンシルバッファから構成されています。

フレームバッファには複数のカラーバッファが存在します。描画する際は対象となるカラーバッファを選択する必要があります。フレームバッファに存在しないカラーバッファを選択するとエラーになります。glReadBuffer でカラーバッファから情報を読み出す場合も同様です。

フレームバッファには OpenGL コンテキストを作成した際にデフォルトで用意されているものglGenFramebuffers で作成したものがあります。ここでは新規にフレームバッファを作成していないため、描画されたフレームバッファはデフォルトのものということになります。

pBuffer を利用してオフスクリーンレンダリングを行っている際は不要ですが、オンスクリーンレンダリングで実際の画面に表示する場合は画面のちらつきを抑えることが求められます。例えば FRONT と BACK に複数のカラーバッファを用意して描画は BACK に対して行い eglSwapBuffers で FRONT に内容をコピーすることでちらつきを抑えることができます。これをダブルバッファリングとよびます。

GL_FRONT = 0x0404
GL_BACK = 0x0405
glReadBuffer = loadGl('glReadBuffer', None, GLenum)
glReadBuffer(GL_BACK)
if not glGetError() == GL_NO_ERROR:
    raise Exception('glReadBuffer failed')

NumPy で用意した配列に、カラーバッファの内容を glReadPixels で読み出します。

GLint = c_int
GLsizei = c_int
GLvoid_p = c_void_p
GL_RGB = 0x1907
GL_UNSIGNED_BYTE = 0x1401

glReadPixels = loadGl('glReadPixels', None, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, GLvoid_p)

x,y = 0,0
width,height = 100,100
format = GL_RGB
type = GL_UNSIGNED_BYTE
image = zeros((height, width, 3), dtype=uint8)

glReadPixels(GLint(x), GLint(y), GLsizei(width), GLsizei(height), format, type, image.ctypes.data_as(GLvoid_p))
if not glGetError() == GL_NO_ERROR:
    raise Exception('glReadPixels failed')

成功すると以下のような画像が得られます。OpenCV の関数でファイルに保存できます

import cv2 as cv
cv.imwrite('green.png', image)

Uploaded Image

Matplotlib でも確認できます。

import matplotlib.pyplot as plt
plt.imshow(image)
plt.show()

リソース解放 (EGL) eglDestroySurfaceeglDestroyContexteglTerminate

EGL_NO_SURFACE = EGLSurface(0)

eglDestroySurface = loadEgl('eglDestroySurface', EGLBoolean, EGLDisplay, EGLSurface)
eglDestroyContext = loadEgl('eglDestroyContext', EGLBoolean, EGLDisplay, EGLContext)
eglTerminate = loadEgl('eglTerminate', EGLBoolean, EGLDisplay)

# カレントの終了
if not eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT):
    print('eglMakeCurrent failed')

# サーフィスの破棄
if not eglDestroySurface(display, surface):
    print('eglDestroySurface failed')

# コンテキストの破棄
if not eglDestroyContext(display, context):
    print('eglDestroyContext failed')

# ディスプレイの解放
if not eglTerminate(display):
    print('eglTerminate failed')
Related pages
    概要 FFI (Foreign Function Interface) の一つである ctypes を利用すると、C 言語のライブラリを Python から利用できます。サンプルコードを記載します。 適宜参照するための公式ドキュメント libm の sqrt を利用する例 main.py #!/usr/bin/python # -*- coding: utf-8 -*- from c
    概要 OpenGL のフレームバッファには複数のカラーバッファを割り当てることができます。フレームバッファはカラーバッファの他にデプスバッファとステンシルバッファを持ちます。これらバッファはすべてレンダーバッファとよばれるメモリ領域です。フラグメントシェーダで out として得られる出力はカラーバッファに格納されます。一つのフラグメントシェーダに