開発中エンジンの描画に用いているのはOpenGL ES 2.0であり、これをWindows上で使うためにGoogle ANGLEというGLES2/3ライブラリを使っている。
これは元々GoogleがWebブラウザChrome上で安定した品質のWebGL対応を行うにあたり、WindowsにおけるOpenGL実装の優劣に左右されないよう作り上げた、DirectXを用いてOpenGL ES 2.0/3.xAPIの動作を模倣するライブラリであるわけなのだけども、同時にiOSやAndroidでサポートされているのと同じGLES2のAPIを使うことができるため、クロスプラットフォームエンジンの開発にも使えるわけだ。
描画APIはこのANGLEで何とかなるし、エンジン自体はiOS/Android/Windowsいずれでも使えるC++で記述されるから良いのだけども、Windows上で開発用のGUIツール周りを実装する際にはわざわざC++で書くのもめんどくさいので、サクッとC#などで作ってしまいたい。しかし、ツール上でGLES2を用いたプレビュー表示を出したいこともあるわけで、このC++で記述されたANGLEによる描画域をC#で組まれた.NET Formアプリ中に埋め込みたい、という需要があった。そんなわけでその方法。
- ANGLEではWin32APIのHWND値をEGLに渡すことで、そのウィンドウ範囲を描画域として初期化する。
- .NET の UserControl は、.Handle プロパティ値としてコントロール自身のHWND値を保持している。
という単純な事実から、
GLES2描画域をUserControlとしてForm中に作り、そのUserControl.Handle値をC++で書かれたUnmanaged codeに渡す。そこでその HWND 値をEGLで初期化することで、GLES2描画域をForm中に作ることができる。
ということになる。Unmanaged codeの呼び出しには System.Runtime.InteropServices を用いる。
こんな感じだろうか。まずはC#側。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Reflection; using System.Runtime.InteropServices; namespace AppNameSpace { public partial class GLES2View : UserControl { [DllImport("GLES2DLL.dll", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr gles2CreateWindow(IntPtr parent_hWnd); [DllImport("GLES2DLL.dll", CallingConvention = CallingConvention.Cdecl)] private static extern void gles2DestroyWindow(IntPtr gles2); private IntPtr envGLES2 = IntPtr.Zero; public GLES2View() { InitializeComponent(); } public void Create() { envGLES2 = gles2CreateWindow(this.Handle); } public void Destroy() { if (envGLES2 != IntPtr.Zero) { gles2DestroyWindow(envGLES2); envGLES2 = IntPtr.Zero; this.Invalidate(); } } } }
上記では省略したけど、Dispose処理中にDestroy()を呼んでやるのを忘れずに。
そしてC++側。C#から呼び出すためにDLLとしてビルドする必要がある。
上記例のC#コードに合わせるのであれば GLES2DLL.dll としてビルドする。
#include "CWin32GLES2.h" // C#からシンボルを指定するため extern "C" で括る extern "C" { // DLL export関数として定義。 __declspec(dllexport) void * gles2CreateWindow(HWND hWnd) { CWin32GLES2 * gles2 = new CWin32GLES2(hWnd); return (void *)gles2; } // DLL export関数として定義。 __declspec(dllexport) void gles2DestroyWindow(void * ptr) { CWin32GLES2 * gles2 = (CWin32GLES2 *)ptr; delete gles2; } }
上記 DLL export 関数では CWin32GLES2 クラスのインスタンスを生成し、そのクラスがHWNDの初期化を行っている。このCWin32GLES2は以下のような感じ。traditionalな書き方になっているのはご容赦願いたい。
まずはヘッダ。
#ifndef CWin32GLES2_h #define CWin32GLES2_h #include <tchar.h> #include <Windows.h> #include <GLES2/gl2.h> #include <EGL/egl.h> class CWin32GLES2 { protected: HWND m_hWnd; HDC m_hDC; EGLDisplay m_eglDisplay; EGLSurface m_eglSurface; EGLContext m_eglContext; public: CWin32GLES2(HWND hWnd); virtual ~CWin32GLES2(); private: bool createWindow(HWND hWnd); void destroyWindow(); void destroyEGL(); bool createEGL(); }; #endif // CWin32GLES2_h
お次は実装。
#include "CWin32GLES2.h" #include <memory> CWin32GLES2::CWin32GLES2(HWND hWnd) : m_eglDisplay(EGL_NO_DISPLAY) , m_eglSurface(EGL_NO_SURFACE) , m_eglContext(EGL_NO_CONTEXT) , m_hWnd(0) , m_hDC(0) { if (!createWindow(hWnd)) { throw std::bad_alloc(); } if (!createEGL()) { destroyWindow(); throw std::bad_alloc(); } } CWin32GLES2::~CWin32GLES2() { destroyEGL(); destroyWindow(); } void CWin32GLES2::destroyWindow() { if (m_hDC) { ReleaseDC(m_hWnd, m_hDC); m_hDC = 0; } if (m_hWnd) { m_hWnd = 0; } } bool CWin32GLES2::createWindow(HWND hWnd) { m_hWnd = hWnd; m_hDC = GetDC(m_hWnd); if (!m_hDC) { destroyWindow(); return false; } return true; } void CWin32GLES2::destroyEGL() { if (m_eglContext != EGL_NO_CONTEXT) { eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(m_eglDisplay, m_eglContext); m_eglContext = EGL_NO_CONTEXT; } if (m_eglSurface != EGL_NO_SURFACE) { eglDestroySurface(m_eglDisplay, m_eglSurface); m_eglSurface = EGL_NO_SURFACE; } if (m_eglDisplay != EGL_NO_DISPLAY) { eglTerminate(m_eglDisplay); m_eglDisplay = EGL_NO_DISPLAY; } } bool CWin32GLES2::createEGL() { m_eglDisplay = eglGetDisplay(m_hDC); if (m_eglDisplay == EGL_NO_DISPLAY) { m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (m_eglDisplay == EGL_NO_DISPLAY) return false; } if (!eglInitialize(m_eglDisplay, 0, 0)) { m_eglDisplay = EGL_NO_DISPLAY; return false; } const EGLint configs[] = { EGL_RED_SIZE, 5, EGL_GREEN_SIZE, 6, EGL_BLUE_SIZE, 5, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 8, EGL_STENCIL_SIZE, EGL_DONT_CARE, EGL_SAMPLE_BUFFERS, 0, EGL_NONE }; EGLConfig configuration; EGLint numFoundConfigurations = 0; if (!eglChooseConfig(m_eglDisplay, configs, &configuration, 1, &numFoundConfigurations)) { destroyEGL(); return false; } if (numFoundConfigurations < 1) { destroyEGL(); return false; } m_eglSurface = eglCreateWindowSurface(m_eglDisplay, configuration, static_cast<EGLNativeWindowType>(m_hWnd), 0); if (!m_eglSurface) { destroyEGL(); return false; } eglBindAPI(EGL_OPENGL_ES_API); EGLint attribList[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; m_eglContext = eglCreateContext(m_eglDisplay, configuration, EGL_NO_CONTEXT, attribList); if (m_eglContext == EGL_NO_CONTEXT) { destroyEGL(); return false; } if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)) { destroyEGL(); return false; } return true; }
…と、こんな感じでHWND渡してEGLでいつものように初期化するだけで、UserControl として ANGLE による GLES2 描画を埋め込むことができるようになる。ひとたび描画環境ができてしまえば、あとは既存のコードを同じようにDLLとしてC#環境から呼び出せるようにするだけで、ツール中でエンジンによる描画プレビューを作れてしまうわけだ。まあ、EGLによるHWND初期化部分のコードは概ね人様のパクリなのだけども、だいたい誰が書いても同じようなコードになるわな。
大抵のサンプルでは、描画先のWindowをWin32APIのCreateWindow(Ex)で作った上で同様のEGLによる初期化をやっているのだけども、この「Windowを作ってHWNDを取得する」ところまではC#でFormを作った時点で終わっているので、そこから先だけやってやればいい、というお話。