fJavaScriptを有効にすると、ここにメニューを表示します。
CMakeでUniversal Windows Platform + OpenGL ES Projectをコンパイル

ysflight.com

2013/03/13追記 CMake 3.8rc1から、NuGetで落ちてくる.targetsファイルをCMakeがtarget_link_librariesでライブラリとして認識するようになった。これで、外部のPython Scriptなどに頼らず、Universal Windows + OpenGL ESのプログラムをコンパイルできるようになった。ちなみに、.targetsファイルサポートのソースは、僕が書いたやつが採用されたよ。スタイルとかは結構手直しされちゃったっぽいけど。

[このページの例題コードはここからダウンロード]

クロスプラットフォーム開発の意義は、ソフトウェアを長い時間をかけて成熟させていくことにある。副産物として、ユーザーベースが多くなる。

人によっては、長い時間をかけてソフトウェアを開発することに興味がないかもしれない。ちょっとしたプログラムを書いては捨て、書いては捨てを繰り返す方が好きな人もいるのかもしれない。そういうスタイルの人は、別に苦労してクロスプラットフォームで書く必要は無い。ソフトウェアの寿命のほうがプラットフォームの寿命より短くてもいいのなら、特定のプラットフォーム用にプログラムを書ければそれでいい。

だけど、僕はそのようには教育されてこなかった。ソフトウェア開発は、ピラミッドを作るようなもので、まず、土台となるライブラリを書いて、その上位レイアを書いて、最後にアプリケーションにする。プログラムは、その後も磨いていくことで、頑強で信頼性の高いプログラムになる。そういうもんだと習ったから、実践してきたつもりだ。

僕は、YS Flight Simulatorを1999年から開発してる。フライトシミュレータを書くというのは、自分の夢だった。まして、それが世界中で多くのユーザに使ってもらえて、プログラマー冥利に尽きると思ってる。こんな楽しい趣味をやめるつもりは全くなくて、この先も続けるつもりでいる。ただ、長く続けようと思うと、使えるプラットフォームが問題になってくる。一昔前は、Windowsだけ対応してればあとは誤差範囲だったけど、今となっては、Windowsは数多くのプラットフォームのひとつに過ぎなくなった。

また、マイクロソフトが、Win32に代わる新しいAPIのUWP (Universal Windows Platform API)を推進しようとしている。Windows 8用のAPIはボロボロだったみたいだけど、かなり改良が加わったみたいで、ぱっと見た感じ、よくできている。何より、CでなくC++を前提としてるところがいい。(C#は誤差範囲、というか、遅すぎて話にならないだろ。)。だから、Win32の残り時間は限られている。さすがに、来年とか再来年にWin32が消滅することはないけれど、この先5年とか10年とかいう単位で、徐々に縮小していくと思う。YSFLIGHTや関連プログラムや僕が仕事で書いてるプログラムは、macOS, Linux, Windowsに対応しているけれど、Win32が消滅すると、macOSとLinuxだけになってしまう。まあ、まだ二つ残ってるわけだけど、減るのはなんとなく気持ち悪い。

しかし、UWPにプログラムを移植する上で、大きな問題になるのが、グラフィックスだった。最近のプログラミングで、グラフィックスがプログラムの構造全体にあまりにも大きな影響を与えるのがひとつの問題になってる、と、思う。僕は絵心が無いもんだから、YSFLIGHTのグラフィックスはそんなに最新技術を使ってるわけではないけど、それでも、全体のコードのうちかなりの分量をグラフィックスが占めている。現在のところ、Direct X9, OpenGL 1.1, OpenGL (ES) 2.0に対応しているけど、UWPは、DirectX11と12しかサポートしていなかった。DirectX10以降の何が悪いって、DirectX9と互換性が無い。全然違うものなんだもん。あれはひどいよね。せっかくDirectX9覚えたのに、かなり裏切られた感があった。

マイクロソフトが我が世の春を謳歌して、望月のかけたることもないと思ってた頃は、Windowsの独占力を最大限に利用して、開発者をマイクロソフトの環境に閉じ込めていた。とにかく、なんでもかんでもWindows独自の勝手な仕様のAPIを使わなくてはならなくて、移植性の高いコードを書くのが大変だった。DirectXはその戦略のひとつで、OpenGL用に書いたシェーダーをDirectX用に移植しようと思っただけで、気が遠くなってくる。それも、せめて、移植したら先々のバージョンのWindowsでも使えることを保証してくれるならまだしも、DirectX9からDirectX10への移行で旧バージョンをバッサリ切りやがったから、もはやDirectXは信用できない。DirectXはダメなAPIだ。今まで、「マイクロソフト、なんでこうする!?」とモニタに向かって叫んだことが何度あったことか。アップルも似たようなもんだけどね。

だからマイクロソフトが、Buildカンファレンス(だったっけ?毎年名前変えるんだよね。)で、クロスプラットフォーム開発がどうこうと言い出したときは、僕は信用しなかった。OpenGL ES 2.0ライブラリを出してくるまでは、信用してなかった。どうせ、またうまいこと言って、開発者を囲い込もうとしてるんでしょ?と、思って見てたら、何?OpenGL ES?本気かよ。YSFLIGHT用のポリゴンエディタ、Polygon CrestのグラフィックスはすべてOpenGL (ES) 2.0の範囲内で書いてある。YSFLIGHTもほぼOpenGL (ES) 2.0の範囲でコンパイルできる。これによって、YSFLIGHTその他関連プログラムをUWPに移植できる可能性が出てきた。

マイクロソフトは、開発環境に関して言えば、正しい方向に向かってると思う。市場の独占を失った今となっては、独自仕様のAPIなんかを多くすると開発者を囲い込めるどころか、開発者は逃げていく。なんとかして、開発者を、マイクロソフトの環境に引き戻さなくてはならない。オープンなスタンダードをサポートするというのは、その目的に対して正しい選択と言える。ところで、とくに僕の授業を取ってる学生さんは、僕がオープンソースな人だと勘違いするみたいなんだけど、僕はオープンソースじゃなくオープンスタンダードな人なんだよね。僕はプログラマーがプログラムを売ってお金をもらうのはまっとうなことだと思う。プログラミングを習得するためには、努力と時間と若さと頭髪と社会生活を犠牲にしなくちゃならない。だから、僕はGPLの発想には全面的に反対だ。だけど、開発環境を提供するベンダーは、オープンスタンダードを取り入れていくべきだと思う。他の工業では、例えば、車とか航空機とかでは、部品やら工法やら標準化がかなり進んでいるのに、なぜかソフトウェアの分野では、各社勝手な仕様のものがあまりにも多すぎる。

そういうわけなので、僕はマイクロソフトがOpenGL ES 2.0をサポートしてくれたのは、大賛成。UWPに乗っかってみることにした。

しかし、僕が書いてきたプログラムは、今はすべてCMakeを使ってビルド環境を作っている。まだiOS用をCMakeでビルドできないんだけど、この上UWPまでビルド環境を別に構築するのは不可能。なんとか、CMakeでUWPアプリを作れないか、やってみた。

そのCMakeは、バージョン3.0ぐらいのころから、Universal Windows Platformのサポートが徐々に入っている。このリンクによると、

 CMake <source-directory> -G "Visual Studio 14 2015" -DCMAKE_SYSTEM_NAME=WindowsStore "-DCMAKE_SYSTEM_VERSION=10.0"

このようにタイプすることで、Universal Windows Platform用のビルド環境ができるら。CMake 3.8rc1以降を使うと、NuGetを実行して、パッケージを取ってきて、OpenGL ESライブラリをリンクするまで一連の処理をすべてCMake内で記述できるようになった。

なお、Power Shellから実行する場合は、ピリオド「.」を特殊文字として認識するらしいので、最後の引数は""で括らないとエラーになる。

Nugetとは?

まずは、Visual Studio 2015を使って、OpenGL ESクロスプラットフォームのテンプレートからプロジェクトを作ってみた。たしかに、OpenGL ES 2.0が使えている。が、どうやら、ライブラリとヘッダは、Visual Studio 2015に含まていなくて、nugetとかいうツールを使ってダウンロードしてこなくてはならないようだ。各プロジェクトごとに、NuGet Package Mangerというのを使って、使用するパッケージを指定できるようだ。そして、このプロジェクトの場合、ビルドするときANGLE.WindowsStoreというパッケージをダウンロードしている。

この、NuGet Package Managerでパッケージを指定する手順を、CMakeの中から実行したい。しかし、CMakeのコマンドやプロパティにはNuGetのパッケージをダウンロードしたり、依存関係を記述したりすることは、今のところできないようだ。もう少し、直接的にコマンドを記述しなくてはならないようだ。

しかし、妙なことに、Visual Studio内では、nugetが使えるのに、コマンドラインから利用できる実行ファイルnuget.exeは、Visual Studio 2015のどこにも入っていない。nuget.exeは、別にダウンロードしてこなくてはならないようだ。せめて、microsoft.comかvisualstudio.comドメインからダウンロードできればもう少し安心なんだけど、https://nuget.org からダウンロードしなくてはならない。もう少し考えて欲しかった。

nuget.exeは、ダウンロード後、パスの張ってあるディレクトリに手動でコピーする。

CMakeからNuGetを呼ぶ

nuget.exeをパスの通ったディレクトリにコピーしたら、CMakeを実行するたびに、OpenGL ES 2.0ライブラリを更新するように記述できる。Visual Studio 2015のプロジェクトテンプレートでは、ビルドするたびに、最新版のANGLE.WindowsStoreになるように更新する処理が入っているが、CMakeを使うのであれば、CMakeでビルド環境を作るタイミングで最新版ライブラリをダウンロードすれば十分だと思う。(もちろん、PRE_BUILDコマンドで実行もできると思う。)

トップレベルのCMakeLists.txtに以下のような記述を追加すれば、それを実行することができる。

[とりあえず、最初のトライ]

# OpenGL ES 2.0 for Windows required for compiling for Universal Windows Platform.
# It's great!  Finally Microsoft officially supports OpenGL ES 2.0!
# I'd be happier if full-scale OpenGL 2.0 is supported, but ES 2.0 is good!
if("${CMAKE_SYSTEM_NAME}" STREQUAL "WindowsStore")
    find_program(NUGET_EXE nuget)
    if(NOT NUGET_EXE)
        message("NUGET.EXE not found.")
        message("Unfortunatelly as of 09/13/2016, you cannot install NUGET.EXE as a globally-available command")
        message("from Visual Studio 2015, or from Add/Remove Programs.  I spent about 30 minutes to search for")
        message("the way, but didn't find one.")
        message("You can download NUGET.EXE from:")
        message("    https://dist.nuget.org/index.html")
        message("and copy it to a directory where PATH is pointing.")
        message(FATAL_ERROR "Please install this executable, and run CMake again.")
    endif()
    message("NUGET.EXE found at ${NUGET_EXE}")

    exec_program(${NUGET_EXE}
        ARGS install "ANGLE.WindowsStore" -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/nuget_packages)
    find_path(UWP_ANGLE_GLES_TARGETS_DIR ANGLE.WindowsStore.targets HINTS ${CMAKE_BINARY_DIR}/nuget_packages/ANGLE.WindowsStore/build/native)
    if(NOT UWP_ANGLE_GLES_TARGETS_DIR)
        message(FATAL_ERROR "OpenGL ES library not found in the package.")
    endif()

    set(UWP_ANGLE_GLES_TARGETS ${UWP_ANGLE_GLES_TARGETS_DIR}/ANGLE.WindowsStore.targets)
endif()

まず、CMAKE_SYSTEM_NAMEがWindowsStoreである場合のみ実行すれば良くて、nuget.exeコマンドが見つからない場合は、nuget.exeをインストールするようにエラーを表示する。また、nuget.exeは、普通にダウンロードすると、ANGLE.WindowsStoreに続けてバージョンナンバーをディレクトリ名に追加してしまうが、それだと以後の処理でバージョンナンバーがわからないと正しいディレクトリを指定できない。-ExcludeVersionオプションを追加して、nuget.exeがパッケージのディレクトリ名にバージョンナンバーを追加しないようにする。

そして、OpenGL ESライブラリをダウンロードできたか確認して、できていれば、変数UWP_ANGLE_GLES_TARGETSに.targetsファイル名をセットする。OpenGL ESを利用するライブラリまたは実行ファイルのターゲットに対して、以下の記述を追加すれば、OpenGL ESが使えるようになる。

target_link_libraries(ysgl ${UWP_ANGLE_GLES_TARGETS})

.targetsファイルには、ヘッダパスの情報も含むので、target_include_directoriesで別にヘッダパスを指定する必要が無い。また、CMakeは、ヘッダパスを依存するプロジェクトにも引き継ぐので、このライブラリをtarget_link_librariesでリンクするプロジェクトは自動的にOpenGL ESが使える。

最小のUniversal Windows Appを作ってみる

OpenGL ESライブラリをダウンロードして、使う目処はついたから、次に最小のUniversal Windows Appを作ってみる。まだ、正常に起動しなくていいから、まずコンパイルだけできるようなものを作ろうと思ってやってみた。まず、実行できなくてもいいから、WindowsがUWPアプリとして認識するものを作る最小のファイルは何かを調べる。

CMakeLists.txt

if("${CMAKE_SYSTEM_NAME}" STREQUAL "WindowsStore")
    add_definitions(-DYS_IS_UNIVERSAL_WINDOWS_APP)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /ZW")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /ZW")
endif()

add_executable(UWP_00_MinimumApp main.cpp main.h)

main.cpp

#include "main.h"

using namespace Windows::ApplicationModel::Core;
using namespace Windows::ApplicationModel::Activation;
using namespace Windows::UI::Core;
using namespace Windows::UI::Input;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Graphics::Display;

[Platform::MTAThread]
int main(Platform::Array<Platform::String^>^)
{
    return 0;
}
main.h
#ifndef MAIN_IS_INCLUDED
#define MAIN_IS_INCLUDED

#endif

CMakeLists.txtは、そのうちUniversal Windows App以外にも使うことを想定して、ifでWindowsStoreアプリかどうか判別するように書いた。/ZWオプションは、マイクロソフトの勝手なC++への拡張を有効にするオプション。当然、この勝手な拡張を使うと他の環境で使えなくなる。現実的には、この機能を使う.cppファイルを最小限に抑えて、大半を標準のC++で書くようにするべきだ。この ^ は、ポインタではなくハンドルの意味らしい。しかし、Universal Windows Platform用のアプリを書くには、次のリンクによると、/ZWオプションが必要だそうだ。

http://stackoverflow.com/questions/16698949/how-to-access-namespace-windows

たしかに、C/C++で書いていて、ポインタじゃなくて、ハンドルであってくれれば、ついでに、そのハンドルが有効なのか無効なのか判定することができれば便利なのに、と、思うことはある。だけど、その正しい解決はハンドルのデータタイプを自分で定義して記述することで、後先考えずに勝手な言語使用を追加するのは、傲慢な誤りと言える。ただ、これまで実験したところでは、/ZWオプションを追加しても、すべてのC++11スタンダードの機能は使えるようだ。これまで書いてきたC++のライブラリはWin32に依存するもの以外はほとんどそのまま利用できるようだ。これは評価できると思う。

当然、この例題では、プログラムは開始できても、何もせずにいきなり終わる。実際やってみると、いきなり終わるだけでなく、開始できなかったというエラーが出る。が、最小のプログラムを作るという目的はこれでOKで、次のステップで、起動するとウィンドウを開くだけのアプリにしてみる。

Window開くだけのUWPアプリ

次のステップは、起動してもエラーが出なくて、ウィンドウを閉じるまで走り続けるプログラムにしてみる。

CMakeLists.txt

if("${CMAKE_SYSTEM_NAME}" STREQUAL "WindowsStore")
    add_definitions(-DYS_IS_UNIVERSAL_WINDOWS_APP)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /ZW")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /ZW")
endif()

add_executable(UWP_01_Window main.cpp main.h)

main.cpp

#include <stdio.h>
#include <time.h>

#include "main.h"

using namespace Windows::ApplicationModel::Core;
using namespace Windows::ApplicationModel::Activation;
using namespace Windows::UI::Core;
using namespace Windows::UI::Input;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Graphics::Display;

ref class SingleWindowAppView sealed : public Windows::ApplicationModel::Core::IFrameworkView
{
public:
    typedef SingleWindowAppView THISCLASS;

private:  // Cannot have a public member variable?
    bool isWindowClosed,isWindowVisible;

public:
    SingleWindowAppView();

    virtual void Initialize(Windows::ApplicationModel::Core::CoreApplicationView^ applicationView);
    virtual void SetWindow(Windows::UI::Core::CoreWindow^ window);
    virtual void Load(Platform::String^ entryPoint);
    virtual void Run();
    virtual void Uninitialize();

    virtual void OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args);
    virtual void OnVisibilityChanged(CoreWindow^ sender, VisibilityChangedEventArgs^ args);
    virtual void OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args);
};

SingleWindowAppView::SingleWindowAppView()
{
    isWindowClosed=false;
    isWindowVisible=false;
}

void SingleWindowAppView::Initialize(Windows::ApplicationModel::Core::CoreApplicationView^ applicationView)
{
    applicationView->Activated+=
        ref new TypedEventHandler<CoreApplicationView^, IActivatedEventArgs^>(this,&THISCLASS::OnActivated);
}
void SingleWindowAppView::SetWindow(Windows::UI::Core::CoreWindow^ window)
{
    window->VisibilityChanged+=
        ref new TypedEventHandler<CoreWindow^,VisibilityChangedEventArgs^>(this,&THISCLASS::OnVisibilityChanged);
    window->Closed+=
        ref new TypedEventHandler<CoreWindow^,CoreWindowEventArgs^>(this,&THISCLASS::OnWindowClosed);
}
void SingleWindowAppView::Load(Platform::String^ entryPoint)
{
}
void SingleWindowAppView::Run()
{
    while(!isWindowClosed)
    {
        if (isWindowVisible)
        {
            // This doesn't seem to wait for an event.  Like a PeekMessage
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
        }
        else
        {
            // This seems to wait until an event is thrown.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
}
void SingleWindowAppView::Uninitialize()
{
}


void SingleWindowAppView::OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args)
{
    CoreWindow::GetForCurrentThread()->Activate();
}
void SingleWindowAppView::OnVisibilityChanged(CoreWindow^ sender, VisibilityChangedEventArgs^ args)
{
    isWindowVisible=args->Visible;
}
void SingleWindowAppView::OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args)
{
    isWindowClosed=true;
}


ref class SingleWindowAppSource sealed : Windows::ApplicationModel::Core::IFrameworkViewSource
{
public:
    virtual Windows::ApplicationModel::Core::IFrameworkView^ CreateView()
    {
        return ref new SingleWindowAppView();
    }
};


[Platform::MTAThread]
int main(Platform::Array<Platform::String^>^)
{
    auto singleWindowAppSource = ref new SingleWindowAppSource();
    CoreApplication::Run(singleWindowAppSource);
    return 0;
}
main.h
#ifndef MAIN_IS_INCLUDED
#define MAIN_IS_INCLUDED

#endif

このうち、

この関数は、Pure Virtual Functionらしく、実装しないとエラーになる。また、OpenGL ESの例題では、ウィンドウが見えているときと見えないときで、メッセージの処理を切り替えているようで、それを覚えておくためには、OnVisibilityChangeを記述しなくてはならないらしい。この関数はVirtual Functionではなく、イベントハンドラとして、window->VisibilityChangedに登録して使うようだ。この方法だと、多分関数名はOnVisibilityChangeでなくてもいいんだな。

OpenGL ESをEnable

最小アプリを書いてわかったことは、Universal Windows PlatformアプリをCMakeで作るとき、ファイルが3本あれば(main.h無くてもいいから2本)いいだけ。非常に良い。あとは、OpenGL ESをリンクすればいいだけ。しかし、OpenGL ES 2.0を使うには、GLSLプログラムが必要で、例題が長くなって、要点が見えなくなってしまう。ということはまったくなくて、glClearColorとglClearを使えば、ウィンドウの色だけ変更することはできる。これによって、OpenGL ES 2.0のコンテキストが正常に動作していることは確認できる。(せめてテクスチャをウィンドウに転送する関数が、GLSL無しでできてくれればねー。非標準関数でできるっぽいけど。)

トップレベルのCMakeLists.txtでUWP_ANGLE_GLES_TARGETSに.targetsファイルをセットしてあれば、あとは、target_link_librariesするだけ。

この3本のファイルだけで、CMakeをかけて実行すると、背景が青くなるので、OpenGL ESのコンテキストが生成できて利用可能になっていることを確認できる。

CMakeLists.txt

cmake_minimum_required(VERSION 3.8)

add_definitions(-DYS_IS_UNIVERSAL_WINDOWS_APP)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /ZW")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /ZW")

# OpenGL ES 2.0 for Windows required for compiling for Universal Windows Platform.
# It's great!  Finally Microsoft officially supports OpenGL ES 2.0!
# I'd be happier if full-scale OpenGL 2.0 is supported, but ES 2.0 is good!
if("${CMAKE_SYSTEM_NAME}" STREQUAL "WindowsStore")
    find_program(NUGET_EXE nuget)
    if(NOT NUGET_EXE)
        message("NUGET.EXE not found.")
        message("Unfortunatelly as of 09/13/2016, you cannot install NUGET.EXE as a globally-available command")
        message("from Visual Studio 2015, or from Add/Remove Programs.  I spent about 30 minutes to search for")
        message("the way, but didn't find one.")
        message("You can download NUGET.EXE from:")
        message("    https://dist.nuget.org/index.html")
        message("and copy it to a directory where PATH is pointing.")
        message(FATAL_ERROR "Please install this executable, and run CMake again.")
    endif()
    message("NUGET.EXE found at ${NUGET_EXE}")

    exec_program(${NUGET_EXE}
        ARGS install "ANGLE.WindowsStore" -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/nuget_packages)
    find_path(UWP_ANGLE_GLES_TARGETS_DIR ANGLE.WindowsStore.targets HINTS ${CMAKE_BINARY_DIR}/nuget_packages/ANGLE.WindowsStore/build/native)
    if(NOT UWP_ANGLE_GLES_TARGETS_DIR)
        message(FATAL_ERROR "OpenGL ES library not found in the package.")
    endif()

    set(UWP_ANGLE_GLES_TARGETS ${UWP_ANGLE_GLES_TARGETS_DIR}/ANGLE.WindowsStore.targets)
endif()

add_executable(UWP_02_glClear main.cpp main.h)
target_link_libraries(UWP_02_glClear ${UWP_ANGLE_GLES_TARGETS})

main.cpp

#include <stdio.h>
#include <time.h>
#include <vector>

#include <agile.h>

#if !defined GL_GLEXT_PROTOTYPES
    #define GL_GLEXT_PROTOTYPES
#endif

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#define YS_GL_ES2
typedef double GLdouble;

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <EGL/eglplatform.h>
#include <angle_windowsstore.h>


#include "main.h"

using namespace Windows::ApplicationModel::Core;
using namespace Windows::ApplicationModel::Activation;
using namespace Windows::UI::Core;
using namespace Windows::UI::Input;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Graphics::Display;
using namespace Windows::System;

using namespace Platform;



ref class SingleWindowAppView sealed : public Windows::ApplicationModel::Core::IFrameworkView
{
public:
    typedef SingleWindowAppView THISCLASS;

private:  // Cannot have a public member variable?
    bool isWindowClosed,isWindowVisible;
    Platform::Agile<CoreWindow> hWnd;

    static THISCLASS ^CurrentView;

    EGLDisplay mEglDisplay;
    EGLContext mEglContext;
    EGLSurface mEglSurface;

public:
    static THISCLASS ^GetCurrentView(void);

    SingleWindowAppView();

    virtual void Initialize(Windows::ApplicationModel::Core::CoreApplicationView^ applicationView);
    virtual void SetWindow(Windows::UI::Core::CoreWindow^ window);
    virtual void Load(Platform::String^ entryPoint);
    virtual void Run();
    virtual void Uninitialize();

    virtual void OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args);
    virtual void OnVisibilityChanged(CoreWindow^ sender, VisibilityChangedEventArgs^ args);
    virtual void OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args);

private:
    void InitializeOpenGLES2(void);
    EGLBoolean InitializeEGLDisplayDefault(void);
    EGLBoolean InitializeEGLDisplay(int level);
    std::vector <EGLint> MakeDisplayAttribute(int level) const;
    void SetUpConfigAttrib(EGLint configAttrib[],int depthBit);
    void CleanUpOpenGLES2(void);
};

/* static */ SingleWindowAppView ^SingleWindowAppView::CurrentView;

/* static */ SingleWindowAppView ^SingleWindowAppView::GetCurrentView(void)
{
    return CurrentView;
}

SingleWindowAppView::SingleWindowAppView()
{
    isWindowClosed=false;
    isWindowVisible=false;

    mEglDisplay=EGL_NO_DISPLAY;
    mEglContext=EGL_NO_CONTEXT;
    mEglSurface=EGL_NO_SURFACE;

    CurrentView=this;
}

void SingleWindowAppView::Initialize(Windows::ApplicationModel::Core::CoreApplicationView^ applicationView)
{
    applicationView->Activated+=
        ref new TypedEventHandler<CoreApplicationView^, IActivatedEventArgs^>(this,&THISCLASS::OnActivated);
}
void SingleWindowAppView::SetWindow(Windows::UI::Core::CoreWindow^ window)
{
    hWnd=window;

    window->VisibilityChanged+=
        ref new TypedEventHandler<CoreWindow^,VisibilityChangedEventArgs^>(this,&THISCLASS::OnVisibilityChanged);
    window->Closed+=
        ref new TypedEventHandler<CoreWindow^,CoreWindowEventArgs^>(this,&THISCLASS::OnWindowClosed);

    InitializeOpenGLES2(); // Example initializes OpenGL ES context in here.
}
void SingleWindowAppView::Load(Platform::String^ entryPoint)
{
    // Example creates renderers here.
}
void SingleWindowAppView::Run()
{
    while(!isWindowClosed)
    {
        if (isWindowVisible)
        {
            // This doesn't seem to wait for an event.  Like a PeekMessage
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            glClearColor(0,0,1,0);
            glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

            if (eglSwapBuffers(mEglDisplay, mEglSurface) != GL_TRUE)
            {
                CleanUpOpenGLES2();
                InitializeOpenGLES2();
                // Example is cleaning and then creating renderers here.
            }
        }
        else
        {
            // This seems to wait until an event is thrown.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }

    CleanUpOpenGLES2(); // Example deletes OpenGL context here.
}
void SingleWindowAppView::Uninitialize()
{
}

void SingleWindowAppView::OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args)
{
    CoreWindow::GetForCurrentThread()->Activate();
}
void SingleWindowAppView::OnVisibilityChanged(CoreWindow^ sender, VisibilityChangedEventArgs^ args)
{
    isWindowVisible=args->Visible;
}
void SingleWindowAppView::OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args)
{
    isWindowClosed=true;
}

void SingleWindowAppView::InitializeOpenGLES2(void)
{
    // Cut & Pasted from OpenGL ES2 example.
    // Modified so that it selects the maximum depth bits.
    EGLint configAttributes[]=
    {
        EGL_RED_SIZE,8,
        EGL_GREEN_SIZE,8,
        EGL_BLUE_SIZE,8,
        EGL_ALPHA_SIZE,8,
        EGL_DEPTH_SIZE,24,
        EGL_STENCIL_SIZE,8,
        EGL_NONE
    };

    const EGLint contextAttributes[]=
    {
        EGL_CONTEXT_CLIENT_VERSION,2,
        EGL_NONE
    };

    const EGLint surfaceAttributes[]=
    {
        EGL_ANGLE_SURFACE_RENDER_TO_BACK_BUFFER,EGL_TRUE,
        EGL_NONE
    };

    EGLConfig config = NULL;

    // According to the example, the program should try to initialize OpenGL ES context with
    // EGL to D3D11 Feature Level 10_0+ first.  If it fails, try EGL to D3D11 Feature Level 9_3.
    // If it fails, try EGL to D3D11 Feature Level 11_0 on WARP.
    if(InitializeEGLDisplayDefault()!=EGL_TRUE &&
       InitializeEGLDisplay(0)!=EGL_TRUE &&
       InitializeEGLDisplay(1)!=EGL_TRUE &&
       InitializeEGLDisplay(2)!=EGL_TRUE)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to initialize EGL");
    }

    // The sample code given with the OpenGL ES template uses 8-bit Z-buffer, 
    // which is pretty much useless.
    // Ideallly want to have 64-bit (or as precise as coordinates, in many cases 32-bit), 
    // but here get whatever available.
    EGLint numConfigs = 0;
    for(int depthBit=32; 8<=depthBit; depthBit-=8)
    {
        SetUpConfigAttrib(configAttributes,depthBit);
        if(eglChooseConfig(mEglDisplay,configAttributes,&config,1,&numConfigs)==EGL_TRUE && 0!=numConfigs)
        {
            printf("%d Depth Bits\n",depthBit);
            break;
        }
    }
    if(0==numConfigs)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to choose first EGLConfig");
    }

    PropertySet^ surfaceCreationProperties = ref new PropertySet();
    surfaceCreationProperties->Insert(ref new String(EGLNativeWindowTypeProperty), hWnd.Get());

    mEglSurface=eglCreateWindowSurface(mEglDisplay,config,reinterpret_cast<IInspectable*>(surfaceCreationProperties),surfaceAttributes);
    if(mEglSurface==EGL_NO_SURFACE)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to create EGL fullscreen surface");
    }

    mEglContext=eglCreateContext(mEglDisplay,config,EGL_NO_CONTEXT,contextAttributes);
    if(mEglContext==EGL_NO_CONTEXT)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to create EGL context");
    }

    if(eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)==EGL_FALSE)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to make fullscreen EGLSurface current");
    }
}

EGLBoolean SingleWindowAppView::InitializeEGLDisplayDefault(void)
{
    // The sample code tells that eglGetPlatformDisplayEXT is use in place for eglGetDisplay to pass display attributes.
    // But, why not trying eglGetDisplay first?
    mEglDisplay=eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if(EGL_NO_DISPLAY!=mEglDisplay)
    {
        auto b=eglInitialize(mEglDisplay,NULL,NULL);
        printf("Looks like the default eglGetDisplay is good enough.  Isn't it?");
        return b;
    }
    return EGL_FALSE;
}

EGLBoolean SingleWindowAppView::InitializeEGLDisplay(int level)
{
    PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT=
        reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(eglGetProcAddress("eglGetPlatformDisplayEXT"));
    if (!eglGetPlatformDisplayEXT)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to get function eglGetPlatformDisplayEXT");
    }

    auto attrib=MakeDisplayAttribute(level);

    mEglDisplay=eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE,EGL_DEFAULT_DISPLAY,attrib.data());
    if(mEglDisplay!=EGL_NO_DISPLAY)
    {
        return eglInitialize(mEglDisplay,NULL,NULL);
    }
    return EGL_FALSE;
}

std::vector <EGLint> SingleWindowAppView::MakeDisplayAttribute(int level) const
{
    const EGLint commonDisplayAttributePre[]=
    {
        EGL_PLATFORM_ANGLE_TYPE_ANGLE,EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
    };
    const EGLint commonDisplayAttributePost[]=
    {
        EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER,EGL_TRUE,
        EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE,EGL_TRUE,
        EGL_NONE,
    };

    std::vector <EGLint> attrib;
    for(auto v : commonDisplayAttributePre)
    {
        attrib.push_back(v);
    }
    switch(level)
    {
    case 0:
        break;
    case 1:  // FL9.  I don't know exactly what FL9 stands for, but as example says.
        attrib.push_back(EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE);
        attrib.push_back(9);
        attrib.push_back(EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE);
        attrib.push_back(3);
        break;
    case 2:  // warp.  I don't know exactly what it means by "warp", but as example says.
        attrib.push_back(EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE);
        attrib.push_back(EGL_PLATFORM_ANGLE_DEVICE_TYPE_WARP_ANGLE);
        break;
    }
    for(auto v : commonDisplayAttributePost)
    {
        attrib.push_back(v);
    }
    return attrib;
}

void SingleWindowAppView::SetUpConfigAttrib(EGLint configAttrib[],int depthBit)
{
    for(int i=0; configAttrib[i]!=EGL_NONE; i+=2)
    {
        if(EGL_DEPTH_SIZE==configAttrib[i])
        {
            configAttrib[i+1]=depthBit;
        }
    }
}

void SingleWindowAppView::CleanUpOpenGLES2(void)
{
    // Cut & Pasted from OpenGL ES2 Example.
    if(mEglDisplay!=EGL_NO_DISPLAY && mEglSurface!=EGL_NO_SURFACE)
    {
        eglDestroySurface(mEglDisplay,mEglSurface);
        mEglSurface=EGL_NO_SURFACE;
    }
    if(mEglDisplay!=EGL_NO_DISPLAY && mEglContext!=EGL_NO_CONTEXT)
    {
        eglDestroyContext(mEglDisplay,mEglContext);
        mEglContext=EGL_NO_CONTEXT;
    }
    if(mEglDisplay!=EGL_NO_DISPLAY)
    {
        eglTerminate(mEglDisplay);
        mEglDisplay=EGL_NO_DISPLAY;
    }
}

ref class SingleWindowAppSource sealed : Windows::ApplicationModel::Core::IFrameworkViewSource
{
public:
    virtual Windows::ApplicationModel::Core::IFrameworkView^ CreateView()
    {
        return ref new SingleWindowAppView();
    }
};

[Platform::MTAThread]
int main(Platform::Array<Platform::String^>^)
{
    auto singleWindowAppSource = ref new SingleWindowAppSource();
    CoreApplication::Run(singleWindowAppSource);
    return 0;
}

main.h (一応書いたけど、無くてもよかったな。)

#ifndef MAIN_IS_INCLUDED
#define MAIN_IS_INCLUDED

#endif

おまけ:Bouncing Ball

画面を蒼くするだけではあんまりなので、ysglライブラリを使って、ボール100個がウィンドウ内をはずむ、Bouncing Ballの例題を作ってみた。これは、僕が授業で例題で学生さんに見せるコードをUWPに移植したもの。適当なビルドディレクトリを作って、ビルドディレクトリがカレントな状態で、CMakeをかけて (上のコマンドを参照)、その後、同じカレントディレクトリから、中に入ってるcorrectCMakeBug.pyを実行して(なので、Pythonが必要)、Project.slnからコンパイルして実行できるようになる。

次のステップは?

上の例題は、CMakeを使ってUniversal Windows Platformアプリを開発できるようになるけれど、それだけだと、CMakeを使う意味はあまりない。プロプライエタリな.slnとか.vcxprojとかいうファイルを管理しなくて済むようになるだけでも意味が無いわけではないけれど、クロスプラットフォーム開発ができないと意味が無い。

自分としては、YSFLIGHTとPolygon CrestとScenery EditorをUWPに乗せるという目標があって、それぞれのプログラムで使ってる枠組み(FsLazyWindow framework。ここからダウンロードできるようになってる。)で、UWP独自のクラスをカバーしようと思ってる。

ただ、ここで上げた例題を参考にして、クロスプラットフォーム開発用のCMakeLists.txtを記述することは可能なので、C++でクロスプラットフォーム開発したいと思ってる人は参考にしてほしい。

ところで、テクスチャに文字を書きたかったら、Win2D.uwpライブラリを使うといい。"uwp" "render" "text"とかいうキーワードで探すと、TextBlockとかいうクラスにたどり着くけど、このクラスは、IFrameworkViewから利用できない。TextBlockを使うには、Xamlなる枠組みでプログラムを書かなくちゃならない。Xamlは、ある種類のアプリにはいいけど、クロスプラットフォーム開発にはまったく不適だから、UWPと心中する覚悟があるのでなければ使うべきではない。Win2Dを使えばいいということがわかるまでにほぼ48時間もかかってしまった。

ちなみに、Win2Dは、コンパクトで依存関係もクリーンで、非常によくできている。

Comments are welcome.  Send E-Mail to: 

Back to http://www.ysflight.com