組み込み向けグラフィックスを試してみた（OpenGL ES 2.0 + EGL）

# はじめに
組み込み向けグラフィックスまわりの開発に携わったときに，その前準備としてお試しコードを書いてみたときの記録です．
キーワードとしては以下を聞いていたので，それらをとっかかりに調べながら簡単なコードを書いてみました．

* OpenGL ES 2.0
* EGL

スタート時の知識・経験は，昔にデスクトップ環境でOpenGLを使って三角形やティーポットを描いたことがあるといった程度です．

# OpenGL ES・EGLとは？

GLESとかEGLって言葉は聞き覚えがなかったのでとりあえず検索．[こちら](https://sites.google.com/site/learningopengl/eglbasics)や[こちら](http://www.asahi-net.or.jp/~yw3t-trns/opengl/opengles/)を見ながらなんとなく内容を把握しました．どちらも組み込み向けというところで出てきているキーワードですね．

> OpenGL ES (OpenGL for Embedded Systems) とは、組み込み用途向け OpenGL のサブセット版です。
> OpenGL ES を利用するために、ネイティブ・プラットフォーム・グラフィックス・インターフェイスの EGL が用意されています。

# ウィンドウを表示
細かなことはともかくとして，コードを書いて動かしてみようといういことで，まずはウィンドウの表示を行いました．書籍等を読んできっちりと内容を落としこんでいくよりも，とりあえず簡単なコードを一度書いてみることで分かった気になろうというモチベーションです．
LinuxデスクトップでもGLES2やEGLを動かすことができそうでしたので，今回はデスクトップ環境で動かしています．Debian 9で試しています．

## ライブラリのインストール
必要なライブラリをインストールします．（libgles2，libegl，xorg-dev）

```
$ sudo apt install libgles2-mesa-dev libegl1-mesa-dev xorg-dev
```

## Windowの生成

`XOpenDisplay()`と`XCreateSimpleWindow()`を使ってDisplayとWindowを生成しています．これらを`initializeEGL()`に渡してEGLの初期化を行っています．今回はLinuxデスクトップ環境でX11を使って動かしましたが，対象の環境に併せてDisplayとWindowを用意してEGLの初期化を行うことになると思います．

```cpp
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <GLES2/gl2.h>
#include <iostream>
#include <unistd.h>

int main(int argc, char *argv[])
{
    Display *xdisplay = XOpenDisplay(nullptr);
    if (xdisplay == nullptr)
    {
        std::cerr << "Error XOpenDisplay." << std::endl;
        exit(EXIT_FAILURE);
    }

    Window xwindow = XCreateSimpleWindow(xdisplay, DefaultRootWindow(xdisplay), 100, 100, 640, 480,
                                         1, BlackPixel(xdisplay, 0), WhitePixel(xdisplay, 0));

    XMapWindow(xdisplay, xwindow);

    EGLDisplay display = nullptr;
    EGLContext context = nullptr;
    EGLSurface surface = nullptr;
    if (initializeEGL(xdisplay, xwindow, display, context, surface) < 0)
    {
        std::cerr << "Error initializeEGL." << std::endl;
        exit(EXIT_FAILURE);
    }

    mainloop(xdisplay, display, surface);

    destroyEGL(display, context, surface);
    XDestroyWindow(xdisplay, xwindow);
    XCloseDisplay(xdisplay);

    return 0;
}
```

## EGLの初期化・破棄

初期化で行っているのは前述のDisplayとWindowを使ってEGLDisplay，EGLContext，EGLSurfaceを生成しています．

```cpp
#include <EGL/egl.h>
#include <X11/Xlib.h>
#include <iostream>

int initializeEGL(Display *xdisp, Window &xwindow, EGLDisplay &display, EGLContext &context, EGLSurface &surface)
{
    display = eglGetDisplay(static_cast<EGLNativeDisplayType>(xdisp));
    if (display == EGL_NO_DISPLAY)
    {
        std::cerr << "Error eglGetDisplay." << std::endl;
        return -1;
    }
    if (!eglInitialize(display, nullptr, nullptr))
    {
        std::cerr << "Error eglInitialize." << std::endl;
        return -1;
    }

    EGLint attr[] = {EGL_BUFFER_SIZE, 16, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE};
    EGLConfig config = nullptr;
    EGLint numConfigs = 0;
    if (!eglChooseConfig(display, attr, &config, 1, &numConfigs))
    {
        std::cerr << "Error eglChooseConfig." << std::endl;
        return -1;
    }
    if (numConfigs != 1)
    {
        std::cerr << "Error numConfigs." << std::endl;
        return -1;
    }

    surface = eglCreateWindowSurface(display, config, xwindow, nullptr);
    if (surface == EGL_NO_SURFACE)
    {
        std::cerr << "Error eglCreateWindowSurface. " << eglGetError() << std::endl;
        return -1;
    }

    EGLint ctxattr[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
    context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxattr);
    if (context == EGL_NO_CONTEXT)
    {
        std::cerr << "Error eglCreateContext. " << eglGetError() << std::endl;
        return -1;
    }
    eglMakeCurrent(display, surface, surface, context);

    return 0;
}
```

また不要になったタイミングでEGLDisplay，EGLContext，EGLSurfaceの破棄を行います．

```cpp
void destroyEGL(EGLDisplay &display, EGLContext &context, EGLSurface &surface)
{
    eglDestroyContext(display, context);
    eglDestroySurface(display, surface);
    eglTerminate(display);
}
```

## メインループ

簡単にですがメインループを用意します．ひとまず`glClear()`して`eglSwapBuffers()`を呼び出しているだけです．

```cpp
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <unistd.h>

void mainloop(Display *xdisplay, EGLDisplay display, EGLSurface surface)
{
    while (true)
    {
        XPending(xdisplay);
        glClearColor(0.25f, 0.25f, 0.5f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        eglSwapBuffers(display, surface);
        usleep(1000);
    }
}
```

## 実行
このような感じでウィンドウが表示されました．

<img width="321" alt="01.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/c3071779-506e-2711-4cfa-9b5c71d908a5.png">

# 三角形を描画
何も描画しないのはさみしいので三角形を描画します．頂点シェーダーとフラグメントシェーダーのプログラムを用意してコンパイル，リンクします．`glVertexAttribPointer()`を使って三角形の頂点を渡し，`glDrawArrays()`で描画を行います．このあたりはOpenGL ES 2.0を勉強していく必要がありそうですが，今回は三角形が描画されたということで満足しておきます．
ちなみに自分が以前にOpenGLを使ってコードを書いた時には，`glBegin()`と`glEnd()`の間で頂点を指定しながら三角形を描画した記憶があったのですが，OpenGL 2.0でGLSL (OpenGL Shading Language)が標準機能となっています．シェーダプログラムについては全く知らなかったので，過去の記憶が逆に邪魔になってちょっとした嵌まりポイントでした．

```cpp
GLuint loadShader(GLenum shaderType, const char *source)
{
    GLuint shader = glCreateShader(shaderType);
    glShaderSource(shader, 1, &source, nullptr);
    glCompileShader(shader);
    GLint compiled = GL_FALSE;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (compiled == GL_FALSE)
    {
        std::cerr << "Error glCompileShader." << std::endl;
        exit(EXIT_FAILURE);
    }
    return shader;
}

GLuint createProgram(const char *vshader, const char *fshader)
{
    GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vshader);
    GLuint fragShader = loadShader(GL_FRAGMENT_SHADER, fshader);
    GLuint program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragShader);
    glLinkProgram(program);
    GLint linkStatus = GL_FALSE;
    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE)
    {
        std::cerr << "Error glLinkProgram." << std::endl;
        exit(EXIT_FAILURE);
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragShader);
    return program;
}

void deleteShaderProgram(GLuint shaderProgram)
{
    glDeleteProgram(shaderProgram);
}

void mainloop(Display *xdisplay, EGLDisplay display, EGLSurface surface)
{
    const char *vshader = R"(
        attribute vec4 vPosition;
        void main() {
            gl_Position = vPosition;
        }
    )";
    
    const char *fshader = R"(
        precision mediump float;
        void main() {
            gl_FragColor = vec4(0.3, 0.8, 0.3, 1.0);
        }
    )";

    GLuint program = createProgram(vshader, fshader);
    glUseProgram(program);
    const GLfloat vertices[] = {0.0f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f};
    GLint gvPositionHandle = glGetAttribLocation(program, "vPosition");
    glEnableVertexAttribArray(gvPositionHandle);
    while (true)
    {
        XPending(xdisplay);

        glClearColor(0.25f, 0.25f, 0.5f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glVertexAttribPointer(gvPositionHandle, 2, GL_FLOAT, GL_FALSE, 0, vertices);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        eglSwapBuffers(display, surface);
        usleep(1000);
    }
    deleteShaderProgram(program);
}
```
<img width="321" alt="02.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/dc0a6e5a-558c-cf5c-1a09-5da1ae6fe59e.png">

# 三角形の回転
次は回転行列を使って三角形をくるくる回してみます．回転行列を用意すれば簡単に回転させることができます．
メインループを次のように変更します．バーテックスシェーダで回転行列をかけ合わせるようにしています．回転行列の内容はmatrixで設定しています．[このあたり](https://ja.wikipedia.org/wiki/回転行列)を参考に，今回はy軸で回転させました．真ん中の縦軸を中心にして立体的に三角形が回転します．6秒程度で一回転するようにdegree変数を更新しています．

```cpp
#include <math.h>
#include <algorithm>
#define degree2radian(degree) ((degree * M_PI) / 180.0F)

void mainloop(Display *xdisplay, EGLDisplay display, EGLSurface surface)
{
    const char *vshader = R"(
        attribute vec4 vPosition;
        uniform mediump mat4 mRotation;
        void main() {
            gl_Position = mRotation * vPosition;
        }
    )";

    const char *fshader = R"(
        precision mediump float;
        void main() {
            gl_FragColor = vec4(0.3, 0.8, 0.3, 1.0);
        }
    )";

    GLuint program = createProgram(vshader, fshader);
    glUseProgram(program);
    const GLfloat vertices[] = {0.0f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f};
    GLint gvPositionHandle = glGetAttribLocation(program, "vPosition");
    glEnableVertexAttribArray(gvPositionHandle);
    GLint gmRotationHandle = glGetUniformLocation(program, "mRotation");
    int degree = 0;
    while (true)
    {
        XPending(xdisplay);

        const GLfloat matrix[] = {
            static_cast<GLfloat>(cos(degree2radian(degree))), 0.0f, static_cast<GLfloat>(sin(degree2radian(degree))), 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            static_cast<GLfloat>(-sin(degree2radian(degree))), 0.0f, static_cast<GLfloat>(cos(degree2radian(degree))), 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f};

        glClearColor(0.25f, 0.25f, 0.5f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glVertexAttribPointer(gvPositionHandle, 2, GL_FLOAT, GL_FALSE, 0, vertices);
        glUniformMatrix4fv(gmRotationHandle, 1, GL_FALSE, matrix);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        eglSwapBuffers(display, surface);
        degree = (degree + 1) % 360;
        usleep(16600);
    }
    deleteShaderProgram(program);
}
```

<img width="321" alt="1.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/5bc03cfc-281f-ee68-1450-8e26c5477578.png">　　<img width="321" alt="2.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/7c5d4954-bb01-9325-0726-93916ed22fe7.png">

# 画像データを描画
ここまででまずまず分かった気になれた（？）のですが，今回はOpenGL ESでゴリゴリと描画していくというよりも，ある方法で取得した画像データをそのまま表示するということが必要そうでした．ということでpngファイルから読み込んだ画像をそのままウィンドウに表示するというコードを書きました．

## ライブラリのインストール
pngファイルの読み込みにはlibpngを使用します．

```
$ sudo apt install libpng-dev
```

## pngファイルの読み込み
libpngを使ってpngファイルを読み込みます．画像のデータの他にwidth / heightとalpha値を持っているかどうかの情報を取得します．

```cpp
#include <png.h>
#include <string>
#include <string.h>

struct PngInfo
{
    png_uint_32 width;
    png_uint_32 height;
    unsigned char *data;
    bool has_alpha;
};

PngInfo loadPng(std::string filename)
{
    FILE *fp = fopen(filename.c_str(), "rb");
    if (fp == nullptr)
    {
        std::cerr << "Error fopen." << std::endl;
        exit(EXIT_FAILURE);
    }

    png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    if (png == nullptr)
    {
        std::cerr << "Error png_create_read_struct." << std::endl;
        exit(EXIT_FAILURE);
    }

    png_infop info = png_create_info_struct(png);
    if (info == nullptr)
    {
        std::cerr << "Error png_create_info_struct." << std::endl;
        exit(EXIT_FAILURE);
    }

    if (setjmp(png_jmpbuf(png)))
    {
        std::cerr << "Error png_jmpbuf." << std::endl;
        exit(EXIT_FAILURE);
    }

    png_init_io(png, fp);

    unsigned int sig_bytes = 0;
    png_set_sig_bytes(png, sig_bytes);

    png_read_png(png, info, (PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND), nullptr);

    png_uint_32 width = 0;
    png_uint_32 height = 0;
    int bit_depth = 0;
    int color_type = 0;
    int interlace_type = 0;
    int compression_type = 0;
    int filter_method = 0;
    png_get_IHDR(png, info, &width, &height, &bit_depth, &color_type, &interlace_type,
                 &compression_type, &filter_method);

    unsigned int row_bytes = png_get_rowbytes(png, info);
    unsigned char *data = new unsigned char[row_bytes * height];

    png_bytepp rows = png_get_rows(png, info);
    for (unsigned int i = 0; i < height; ++i)
    {
        memcpy(data + (row_bytes * i), rows[i], row_bytes);
    }

    png_destroy_read_struct(&png, &info, nullptr);

    return {width, height, data, ((color_type == PNG_COLOR_TYPE_RGBA) ? true : false)};
}

void deletePng(PngInfo &png)
{
    delete[] png.data;
}
```

## メインループの変更
メインループを書きなおします．読み込んだ画像データをテクスチャに貼って，そのテクスチャの描画を行います．

```cpp
void mainloopPng(Display *xdisplay, EGLDisplay display, EGLSurface surface)
{
    const char *vshader = R"(
        attribute vec4 position;
        attribute vec2 texcoord;
        varying vec2 texcoordVarying;
        void main() {
            gl_Position = position;
            texcoordVarying = texcoord;
        }
    )";

    const char *fshader = R"(
        precision mediump float;
        varying vec2 texcoordVarying;
        uniform sampler2D texture;
        void main() {
            gl_FragColor = texture2D(texture, texcoordVarying);
        }
    )";

    GLuint program = createProgram(vshader, fshader);
    glUseProgram(program);

    auto png = loadPng("./sample.png");
    GLuint position = glGetAttribLocation(program, "position");
    glEnableVertexAttribArray(position);
    GLuint texcoord = glGetAttribLocation(program, "texcoord");
    glEnableVertexAttribArray(texcoord);

    GLuint textures[] = {static_cast<GLuint>(glGetUniformLocation(program, "texture"))};
    glGenTextures(1, textures);
    glBindTexture(GL_TEXTURE_2D, textures[0]);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexImage2D(GL_TEXTURE_2D, 0, png.has_alpha ? GL_RGBA : GL_RGB, png.width, png.height,
                 0, png.has_alpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, png.data);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    const float vertices[] = {-1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f};
    const float texcoords[] = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f};
    while (true)
    {
        XPending(xdisplay);

        glClearColor(0.0f, 0.0f, 0.0f, 0.1f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 0, texcoords);
        glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 0, vertices);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        eglSwapBuffers(display, surface);
        usleep(1000);
    }

    deletePng(png);
    deleteShaderProgram(program);
}
```
<img width="322" alt="03.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/9a009559-3d59-5c81-8ab5-06b387583642.png">

# おわりに
細かなところはともかくで，OpenGL ES 2.0 + EGLで動くコードを書くことができました．書籍等で詳しく勉強するのもいいですが，まずは動くコードを書いてみるというのも安心感が得られていいと思います．