組み込みの埋まってるとこ

システム寄りの組み込みCプログラミングBLOG

SDLとMinGWによるOpenGL入門環境

SDLによるOpenGLの実行例

Windowsとグラフィックスボード無しのPCでOpenGL入門用の環境を構築してみました。 ANGLEによりDirectX上でOpenGL ESをエミュレートしましたので実行速度も高速です。

動作環境

今回はできるだけ多くのPCで動作することを目標としているのでハードウェアに注目すべき点は特になく、事務処理に使用されるようなPCと同レベルです。

グラフィックスもインテルCPU内蔵のものを使用していますし、 ドライバはWindows 10と一緒にインストールされていたDirectXそのままです。

ツールチェーン

  • MinGW64
  • SDL
  • ANGLE (libGLESv2.dll libEGL.a d3dcompiler_47.dll)
  • GCC
  • make

ソフト開発のツールはmsys2のMinGW64をベースにしています。 Visual Studioは便利なのですが完成した開発環境を配布するとき敷居が高いので候補から外しました。GCCとmakeを使っているのもそのような理由からきています。

OpenGLフレームワークとしてはglfwやglutがよく使われるのですが、 歴史的な資産の多さでSDLを選んでいます。

ANGLEは今回の目玉となるドライバで、OSの基本的なグラフィックスAPIOpenGL ESをエミュレートしてくれる優れものです。 公式にはドライバのバイナリをダウンロードできるサイトは存在していないため、PCにインストールされていたChromeからDLLだけ拾ってきて使用しています。 他にもEdgeやVSCodeから入手しても問題ないと思います。

SDLでANGLEをレンダラーとするプログラム例

上記の環境でOpenGL ESを動作させたプログラムは次のようになります。 まずはプログラムの前半としてSDLOpenGLのサーフェイスをウインドウに表示させる部分から説明します。

#define SDL_ASSERT_LEVEL 3
#include <SDL.h>
#include <GLES2/gl2.h>

#define WINDOW_WIDTH  800
#define WINDOW_HEIGHT 200

void DrawSetup(void);
void Draw(void);
void DrawCleanup(void);

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_EVERYTHING);
    SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);

    /* Window and Renderer */
    SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");

    SDL_Window *win;
    win = SDL_CreateWindow("SDL-MinGW-OpenGL",
                           SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,
                           WINDOW_WIDTH, WINDOW_HEIGHT,
                           SDL_WINDOW_OPENGL);
    SDL_Renderer *rend;
    rend = SDL_CreateRenderer(win,
                              -1,
                              SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);

    /* OpenGL */
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);

    /* Main loop */
    DrawSetup();

    int quit = 0;
    while (!quit) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if(event.type == SDL_QUIT) {
                quit = 1;
                break;
            }
        }

        Draw();
        SDL_Delay(16);
        SDL_GL_SwapWindow(win);
    }

    DrawCleanup();

    /* Finalize */
    SDL_GL_MakeCurrent(win, NULL);
    SDL_DestroyRenderer(rend);
    SDL_DestroyWindow(win);
    SDL_Quit();

    return 0;
}

プログラム全体としてはSDLチュートリアルのようになっていますが、 注目すべきポイントはOpenGLを使用すためにSDLがレンダラーとしてANGLEを選択するように与えているヒントです。

ヒントはSDLの開発者が必要に応じて追加しているようなところがあり、 正しいヒントの組み合わせはSDLソースコードを読まなければ正解が分からないのですが、 今回は試行錯誤を繰り返し次の2つのヒントを与えれば目的を達成できるらしいという結論に至りました。

SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");

これらのヒントを与えたうえでPATHが通ったディレクトリにlibGLESv2.dll libEGL.dll d3dcompiler_47.dllが存在していれば、 プログラム実行時にレンダラーとしてANGLEが選択されOpenGL ESが使用できるようになります。

しかし別途グラフィックスボードが存在する環境やソースコードからコンパイルしたANGLEをリンクした場合は少し違う結果になるかもしれません。 そのような時のために他にも関係ありそうなヒントをメモしておきます。

  • SDL_HINT_VIDEO_WIN_D3DCOMPILER
  • SDL_HINT_RENDER_OPENGL_SHADERS

またWindows環境でSDLがレンダラーを選択する仕組みはSDLのソースツリーにあるdocs/README-windows.mdに詳細があります。

Hello Triangleのプログラム例

先ほどのプログラム例の後半で実際にOpenGL ESで描画している部分は次のとおりです。

unsigned int vertexShader;
unsigned int fragmentShader;
unsigned int programObject;

#define LF "\n"

const char *vShaderStr[1] = {
    "attribute vec4 vPosition;" LF
    "varying vec3 fPosition;" LF
    "void main()" LF
    "{" LF
    "  gl_Position = vPosition - vec4(0.5, 0.5, 0.0, 0.0);" LF
    "  fPosition = vPosition.xyz;" LF
    "}" LF
    LF,
};

const char *fShaderStr[1] = {
    "precision mediump float;" LF
    "varying vec3 fPosition;" LF
    "void main()" LF
    "{" LF
    "  gl_FragColor = vec4(0.0, 0.4, 0.8, 1.0) + vec4(fPosition,0.0);" LF
    "}" LF
    LF,
};

void DrawSetup(void)
{
    /* Vertex shader */
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, vShaderStr, NULL);
    glCompileShader(vertexShader);

    /* Fragment shader */
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, fShaderStr, NULL);
    glCompileShader(fragmentShader);
    
    /* Shader program */
    programObject= glCreateProgram();
    glAttachShader(programObject, vertexShader);
    glAttachShader(programObject, fragmentShader);
    glBindAttribLocation(programObject, 0, "vPosition");
    glLinkProgram(programObject);

    glDeleteShader(fragmentShader);
    glDeleteShader(vertexShader);

    glUseProgram(programObject);

    /* Vertex */
    static GLfloat vVertices[] = {
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f,
        1.0f, 0.0f, 0.0f,
    };
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), vVertices);
    glEnableVertexAttribArray(0);

    /* View */
    glViewport(0,0, WINDOW_WIDTH, WINDOW_HEIGHT);
}

void DrawCleanup(void)
{
    glDeleteProgram(programObject);
}

void Draw(void)
{
    glClearColor(0.0f, 0.0f, 0.2f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

プログラムは有名な教科書のHello Triangleと似たような内容になっていますが、 シェーダーをコンパイルするために必要なDLLがANGLEやDirectXのバージョンによって違うようなのでその確認のため、シェーダー部分はバーテックスシェーダーからフラグメントシェーダーへ簡単なパラメターを渡して演算するように少し変更しています。

MinGW64のパッケージとANGLEのDLL

プログラムをコンパイルする前に必要なMinGW64のパッケージをインストールしておきます。

$ pacman -S mingw-w64-x86_64-SDL mingw-w64-x86_64-mesa

mesaはオープンソースOpenGL互換ライブラリ等のパッケージです。 実はmesaにもANGLEと同じDLLが存在しているのですが、 mesaのDLLはANGLEとは異なりDirectXを使ってOpenGLをエミュレートするようにはなっていないため、 グラフィックスボードが存在しない環境では動作しないようです。 そのため今回はOpenGLのヘッダファイルを使用するためだけにmesaをインストールしています。

DLLは別途Chrome, Edge, VSCode等がインストールされているフォルダから次のDLLをコンパイルした実行プログラムと同じフォルダにコピーしてください。

  • libGLESv2.dll
  • libEGL.dll
  • d3dcompiler_47.dll

実行時にDLLをロードするときmesaのDLLが先に見つかってしまうとおそらくウインドウが生成できないエラーになりますのでPATHの検索順序には気を付けるようにしてください。

MinGWでのコンパイル

プログラムをコンパイルするMakefileは次のようになります。

CFLAGS := $(shell pkg-config --cflags sdl2 gles2 egl)
LIBS   := ./d3dcompiler_47.dll ./libGLESv2.dll ./libEGL.dll $(shell pkg-config --libs sdl2)

all: demo

demo: demo.c
    gcc $^ -g -Og $(CFLAGS) $(LIBS) -o $@

demo.cは仮の名前ですので、実際のファイル名に変更して使用してください。

pkg-configの出力例は次のようになります。もしpkg-configが動作しない場合はこの出力例を参考にして直接指定してください。

CFLAGS := -IC:/msys64/mingw64/include/SDL2 -Dmain=SDL_main -IC:/msys64/mingw64/include -DEGL_NO_X11
LIBS   := ./d3dcompiler_47.dll ./libGLESv2.dll ./libEGL.dll -LC:/msys64/mingw64/lib -lmingw32 -lSDL2main -lSDL2 -mwindows

ここでリンクしているDLLについてですが、Chromeからコピーしてきたものであるため.a (または.lib)が存在しません。 そのため少々荒っぽいですがコピーしてきた.dllを直接リンクしています。

d3dcompiler_47.dllはこの段階でリンクしなくてもエラーにはならず、 libGLESv2.dllが実行時に必要に応じてロードする仕組みになっています。

実行結果

プログラム実行中のタスクマネージャーのGPUの負荷グラフです。

ANGLE実行時のGPU負荷

CPUでレンダリングする部分があると待ち合わせ等でGPUの3Dが100%に張り付くことはないのですが、 今回はプログラムが単純なのですべてCPU内蔵グラフィックスでレンダリングできているようです。

なおANGLEが正しく動作していることを確認するため負荷の測定中はメインループ中にあるSDL_Delayを一時的にコメントアウトしています。 通常使用時は節電と静音のため適度にSDL_Delayを入れておいても十分に高速です。