SSEで画像処理 --- 導入
下記記事にてアラインされたstd::vectorを作成する方法を記述しました。
アラインされたstd::vector - 何でもプログラミング
今回はそれを利用して、SSEで簡単な画像処理を行ってみます。
準備
今回はintrinsicを用いて実装するため、#include
また記述の簡便化のため、aligned_vectorを定義しておきます。
#include <intrin.h> template<class T> using aligned_vector = std::vector<T, AlignedAllocator<T, 16>>;
ただのコピー
レジスタに転送、取得を行うだけのコードになります。
srcのサイズが16の倍数でない場合は、余りを処理するコードが必要ですが、今回は考慮していません。
aligned_vector<byte> Copy(const aligned_vector<byte>& src) { aligned_vector<byte> dst(src.size()); for (int i = 0; i < (int)src.size(); i += 16) { // レジスタに転送 __m128i data = _mm_load_si128((__m128i*)(src.data() + i)); // レジスタから取得 _mm_store_si128((__m128i*)(dst.data() + i), data); } return dst; }
ネガポジ反転
入力データをbit反転することで実現します。
aligned_vector<byte> NegaPosi(const aligned_vector<byte>& src) { aligned_vector<byte> dst(src.size()); // 全bitが1のデータ __m128i c = _mm_set1_epi8((byte)0xff); for (int i = 0; i < (int)src.size(); i += 16) { __m128i px = _mm_load_si128((__m128i*)(src.data() + i)); // bit反転 px = _mm_xor_si128(px, c); _mm_store_si128((__m128i*)(dst.data() + i), px); } return dst; }
CPUの対応確認
CPUがSSEに対応しているかどうかは、__cpuidを用いて確認できます。
次のコードは一例となります。
int cpuInfo[4]; __cpuid(cpuInfo, 1); bool sse2 = (cpuInfo[3] >> 26) & 1; bool sse3 = (cpuInfo[2] >> 0) & 1; bool sse41 = (cpuInfo[2] >> 19) & 1; bool sse42 = (cpuInfo[2] >> 20) & 1; bool avx = (cpuInfo[2] >> 28) & 1;
詳細は下記ページを参照してください。
__cpuid, __cpuidex
アラインされたstd::vector
SSEやAVXを利用して処理を高速化する際に、メモリ領域がアラインされている必要があります。
アラインされた領域を確保するだけであれば_mm_mallocで可能ですが、_mm_freeを自分で呼ぶ必要があります。
今回はstd::vectorでアラインされた領域を確保できるようにしたいと思います。
IsAligned
まず第一にメモリがアラインされているか確認する関数が必要です。
中身は至って簡単で、アドレス値がNで割り切れれば、NByteでアラインされています。
bool IsAligned(void* p, int n) { return (int)p % n == 0; }
AlignedAllocator
std::vectorには独自のアロケータを指定できます。
そこで、アラインされたメモリを確保するアロケータを定義します。
allocate、deallocate以外はほぼ定型文です。(テンプレート引数が型のみであれば、rebindは必要ありません)
template <class T, std::size_t N> struct AlignedAllocator { using value_type = T; AlignedAllocator() {} template<class U> AlignedAllocator(const AlignedAllocator<U, N>&) {} template<class U> bool operator == (const AlignedAllocator<U, N>&) { return true; } template<class U> bool operator != (const AlignedAllocator<U, N>&) { return false; } template<class U> struct rebind { using other = AlignedAllocator<U, N>; }; T* allocate(const size_t n) { return (T*)_mm_malloc(n * sizeof(T), N); } void deallocate(T* p, size_t) { _mm_free(p); } };
利用方法
下記のように利用すると、アラインされたstd::vectorが作成できます。
std::vector<T, AlignedAllocator<T, 16>> vector_aligned16(1); IsAligned(vector_aligned16.data(), 16); // true std::vector<T, AlignedAllocator<T, 128>> vector_aligned128(1); IsAligned(vector_aligned128.data(), 128); // true
Bitmap読み書き
下記記事にて、Windows標準ライブラリで画像を読み書きする方法を記述しました。
GDI+ --- 画像ファイル読み書き - 何でもプログラミング
Bitmapであれば更に何のライブラリも必要なく簡単に読み書きできます。
ちょっとした画像関係の動作確認レベルの時に便利です。
今回は24bitBitmapを読み書きしてみます。
ヘッダ構造体
Bitmapファイルのヘッダ部分をまとめた構造体です。
コンパイラが勝手にパディングを入れないよう、pshpack2、poppackで挟んでいます。(VisualC++専用)
#include <pshpack2.h> struct BitmapHeader { unsigned short bfType; unsigned long bfSize; unsigned short bfReserved1; unsigned short bfReserved2; unsigned long bfOffBits; unsigned int biSize; int biWidth; int biHeight; unsigned short biPlanes; unsigned short biBitCount; unsigned int biCompression; unsigned int biSizeImage; int biXPixPerMeter; int biYPixPerMeter; unsigned int biClrUsed; unsigned int biClrImporant; }; #include <poppack.h>
読み込み
Bitmapの画像データは、幅が4Byteの倍数になるよう格納されています。
今回はデータを詰めて取り出すよう実装しています。
void LoadBitmap24(std::string path, int* width, int* height, std::vector<unsigned char>* pixels) { std::ifstream file(path, std::ios::binary); assert(file.is_open()); BitmapHeader header; file.read((char*)&header, sizeof(header)); int w = header.biWidth; int h = header.biHeight; int step = ToBitmapStep(3 * w); std::vector<unsigned char> originalPixels(step * h); file.read((char*)originalPixels.data(), originalPixels.size()); pixels->resize(3 * w * h); for (int i = 0; i < h; ++i) memcpy(pixels->data() + i * 3 * w, originalPixels.data() + i * step, 3 * w); *width = w; *height = h; } int ToBitmapStep(int step) { int paddings[] = { 0, 3, 2, 1 }; return step + paddings[step % 4]; }
書き込み
ヘッダの内容は、ほとんど決められた値になっています。
画像データを、幅が4Byteの倍数になるよう調整して書き出しています。
void SaveBitmap24(std::string path, int width, int height, const unsigned char* pixels) { std::ofstream file(path, std::ios::binary); assert(file.is_open()); int step = ToBitmapStep(3 * width); BitmapHeader header; header.bfType = 0x4d42; // "BM" header.bfSize = step * height + 54; header.bfReserved1 = 0; header.bfReserved2 = 0; header.bfOffBits = 54; header.biSize = 40; header.biWidth = width; header.biHeight = height; header.biPlanes = 1; header.biBitCount = 24; header.biCompression = 0; header.biSizeImage = 0; header.biXPixPerMeter = 0; header.biYPixPerMeter = 0; header.biClrUsed = 0; header.biClrImporant = 0; file.write((char*)&header, sizeof(header)); std::vector<unsigned char> bmpPixels(step * height); for (int i = 0; i < height; ++i) memcpy(bmpPixels.data() + i * step, pixels + i * 3 * width, 3 * width); file.write((char*)bmpPixels.data(), bmpPixels.size()); }
動作確認
Bitmapを読み込んで、赤青反転の後にBitmap保存をしてみます。
int main() { int width, height; std::vector<byte> pixels; LoadBitmap24("parrots.bmp", &width, &height, &pixels); for (int i = 0; i < width * height; ++i) std::swap(pixels[3 * i], pixels[3 * i + 2]); SaveBitmap24("output.bmp", width, height, pixels.data()); return 0; }
WindowsでOpenGLES --- imgui導入
OpenGL上でGUIを実装するライブラリの一つに、ImGuiがあります。
GitHub - ocornut/imgui: Bloat-free Immediate Mode Graphical User interface for C++ with minimal dependencies
数ファイル取り込むだけでGUIを実装できるため、簡単なソフトの作成には便利です。
今回はGLFW上でImGuiを動作させてみたいと思います。
GLFWに関しましては下記記事を参照してください。
WindowsでOpenGLES --- GLFW導入 - 何でもプログラミング
Immediate Mode GUI
このライブラリは "Immediate Mode GUI" という方式を採用しており、一般的な、ボタンオブジェクトを作ってテキストを変更して…といった "Retained Mode GUI" とは異なります。
Immediate Modeでは毎回GUIを描き直すため、既にあるGUIツリーを更新していくRetained Modeに比べて、わかりやすいコードになる傾向があります。
ただしライブラリ側でstatic変数に状態を持っていたりするため、Immediate Modeが完全優位というわけでもありません。
個人的には毎回GUIの作り直し(一部のみの更新をしない)が重要だと思いますので、Retained ModeであってもwebのReactのようなフレームワークがあれば変わらないと思います。
ファイル取り込み
GitHubからダウンロードし、下記ファイルをプロジェクトに追加します。
imgui_impl_glfw_gl3はexamples/opengl3_example/以下に入っています。
imgui_impl_glfw_gl3.cppの修正
元のままではOpenGL3を利用するようになっていますので、OpenGLES3に対応するよう修正します。
// GLES3/gl3.hをincludeするように -#include <GL/gl3w.h> +#define GLFW_INCLUDE_ES3 +#define GL_GLEXT_PROTOTYPES // GL_BLEND_SRCは存在しないのでGL_BLEND_SRC_ALPHAに -GLint last_blend_src; glGetIntegerv(GL_BLEND_SRC, &last_blend_src); -GLint last_blend_dst; glGetIntegerv(GL_BLEND_DST, &last_blend_dst); +GLint last_blend_src; glGetIntegerv(GL_BLEND_SRC_ALPHA, &last_blend_src); +GLint last_blend_dst; glGetIntegerv(GL_BLEND_DST_ALPHA, &last_blend_dst); // Vertexシェーダのversion変更 -"#version 330\n" +"#version 300 es\n" // Fragmentシェーダのversion変更、precisionの設定 -"#version 330\n" +"#version 300 es\n" +"precision mediump float;\n"
アプリケーションコード
ImGui_ImplGlfwGL3_Initで初期化し、描画ごとにImGui_ImplGlfwGL3_NewFrameとImGui::Render()を呼び出します。
今回はImGui::ShowTestWindowを用いてサンプルを描画しています。
毎フレーム描画する必要がない場合はglfwPollEventsの代わりにglfwWaitEventsを利用してください。
フレームレートはglfwSwapInterval(1)でディスプレイと同期するようになるのですが、Windowsでは呼ばなくてもディスプレイと同期しているようです。
#define GLFW_INCLUDE_ES3 #define GL_GLEXT_PROTOTYPES #include <GLFW\glfw3.h> #include "imgui.h" #include "imgui_impl_glfw_gl3.h" #pragma comment(lib, "glfw3.lib") #pragma comment(lib, "libGLESv2.lib") int main() { assert(glfwInit()); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); GLFWwindow* window = glfwCreateWindow(650, 350, "ImGui", NULL, NULL); assert(window); glfwMakeContextCurrent(window); assert(ImGui_ImplGlfwGL3_Init(window, true)); glfwSetWindowSizeCallback(window, [](GLFWwindow*, int w, int h) { glViewport(0, 0, w, h); }); while (glfwWindowShouldClose(window) == GL_FALSE) { glfwPollEvents(); glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplGlfwGL3_NewFrame(); ImGui::ShowTestWindow(); ImGui::Render(); glfwSwapBuffers(window); } ImGui_ImplGlfwGL3_Shutdown(); glfwTerminate(); return 0; }
WindowsでOpenGLES --- 三角形描画
下記記事にてGLFWで、OpenGLESが初期化されたウィンドウを作成しました。
WindowsでOpenGLES --- GLFW導入 - 何でもプログラミング
今回はとりあえず三角形を描画してみたいと思います。
shared_ptrでリソースの管理
OpenGLではCreateとDeleteをきちんと管理する必要があります。(glCreateProgram() → glDeleteProgram(GLuint)など)
手動で管理するのは面倒なので、shared_ptrに解放を任せてみます。
shared_ptrにはカスタムデリータが設定できるので、そこで解放を行います。
OpenGLにはglCreateProgramのような単体リソース作成と、glGenTexturesのような複数リソース作成の2パターンがあるので、2つ定義してみました。
今回は暗黙キャストが利用したかったのでクラスを定義しましたが、不必要であればshared_ptrそのもので良いと思います。
class GLResource { std::shared_ptr<GLuint> _resource; public: GLResource() {} // glCreateProgram, glDeleteProgramなど用 GLResource(GLuint resource, std::function<void(GLuint)> glDelete) { if (resource != 0) _resource.reset(new GLuint(resource), [=](GLuint* x) { glDelete(*x); delete x; }); } // glGenTextures、glDeleteTextursなど用 GLResource(std::function<void(GLsizei, GLuint*)> glGen, std::function<void(GLsizei, GLuint*)> glDelete) { GLuint resource = 0; glGen(1, &resource); if (resource != 0) _resource.reset(new GLuint(resource), [=](GLuint* x) { glDelete(1, x); delete x; }); } operator bool() { return (bool)_resource; } operator GLuint() { return *_resource; } };
Program(Shader)作成
シェーダコードをコンパイルしてプログラムを作成する関数を定義します。
CompileShader、PrintProgramLogは後述いたします。
GLResource CreateProgram(std::string vertexCode, std::string fragmentCode) { // Vertexシェーダコンパイル GLResource vertexShader(glCreateShader(GL_VERTEX_SHADER), glDeleteShader); assert(vertexShader); CompileShader(vertexShader, vertexCode.c_str()); // Fragmentシェーダコンパイル GLResource fragmentShader(glCreateShader(GL_FRAGMENT_SHADER), glDeleteShader); assert(fragmentShader); CompileShader(fragmentShader, fragmentCode.c_str()); GLResource program(glCreateProgram(), glDeleteProgram); assert(program); // リンクプログラム glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); glLinkProgram(program); PrintProgramLog(program); glDetachShader(program, fragmentShader); glDetachShader(program, vertexShader); // リンクエラー確認 GLint status; glGetProgramiv(program, GL_LINK_STATUS, &status); assert(status != GL_FALSE); return program; }
CompileShader
void CompileShader(GLuint shader, const GLchar* code) { glShaderSource(shader, 1, &code, NULL); glCompileShader(shader); PrintShaderLog(shader); // コンパイルエラー確認 GLint status; glGetShaderiv(shader, GL_COMPILE_STATUS, &status); assert(status != GL_FALSE); } void PrintShaderLog(GLuint shader) { GLint length; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); if (0 < length) { std::vector<GLchar> log(length); glGetShaderInfoLog(shader, log.size(), &length, log.data()); printf("%s\n", log.data()); } }
PrintProgramLog
void PrintProgramLog(GLuint program) { GLint length; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); if (0 < length) { std::vector<GLchar> log(length); glGetProgramInfoLog(program, log.size(), &length, log.data()); printf("%s\n", log.data()); } }
main関数
入力ポリゴンを赤色に塗りつぶすだけのGLSLで、三角形を描画しています。
ウィンドウのサイズ変更に追従するよう、コールバックを登録しています。
GLFWのコールバック登録は関数ポインタであり、ローカル変数キャプチャのためにdrawグローバル変数を用意しました。
std::function<void()> draw; int main() { assert(glfwInit()); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); GLFWwindow* window = glfwCreateWindow(200, 200, "Triangle", NULL, NULL); assert(window); glfwMakeContextCurrent(window); // Program準備 std::string vertexCode = "#version 300 es\n" "layout (location = 0) in vec4 position;\n" "void main() { gl_Position = position; }"; std::string fragmentCode = "#version 300 es\n" "precision mediump float;\n" "out vec4 fragColor;\n" "void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); }"; GLResource program = CreateProgram(vertexCode, fragmentCode); glUseProgram(program); // 頂点データ設定 glEnableVertexAttribArray(0); const GLfloat positions[] = { 0.0, 1.0, 1.0, -1.0, -1.0, -1.0 }; glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, positions); // Draw関数定義 draw = [&] { glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLES, 0, 3); glfwSwapBuffers(window); }; // コールバック登録 glfwSetWindowSizeCallback(window, [](GLFWwindow*, int w, int h) { glViewport(0, 0, w, h); }); glfwSetWindowRefreshCallback(window, [](GLFWwindow*) { draw(); }); // メインループ while (glfwWindowShouldClose(window) == GL_FALSE) { glfwWaitEvents(); } glfwTerminate(); return 0; }
WindowsでOpenGLES --- GLFW導入
下記の記事にてWIN32APIでOpenGLESを利用する方法を記述しました。
WindowsでOpenGLES --- ANGLE準備 - 何でもプログラミング
今回は、ウィンドウの作成やOpenGLESの初期化などをクロスプラットフォームに行ってくれるライブラリ、GLFWを導入してみたいと思います。
ダウンロード
下記ページより必要なdll(glfw3.dll)を取得します。(ソースをCMakeでビルドする方法でも問題ありません。)
GLFW - Download
GLFW初期化
OpenGLES3を有効化するために、いくつか設定を行います。
あとはglfwCreateWindowを呼ぶだけで、必要な初期化を全て行ってくれます。
#define GLFW_INCLUDE_ES3 #define GL_GLEXT_PROTOTYPES #include <GLFW\glfw3.h> #pragma comment(lib, "glfw3.lib") #pragma comment(lib, "libGLESv2.lib") assert(glfwInit()); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); GLFWwindow* window = glfwCreateWindow(300, 300, "GL Window", NULL, NULL); assert(window); glfwMakeContextCurrent(window);
Draw関数
ウィンドウを灰色に塗りつぶすだけの関数です。
void Draw(GLFWwindow* window) { glClearColor(0.5, 0.5, 0.5, 1); glClear(GL_COLOR_BUFFER_BIT); glfwSwapBuffers(window); }
Run
下記のようにイベントループを回します。
また再描画が必要な時にDrawを呼ぶよう、コールバックをアタッチしています。
実行すると灰色に塗りつぶされたウィンドウが表示されます。
glfwSetWindowRefreshCallback(window, Draw);
while (glfwWindowShouldClose(window) == GL_FALSE)
{
glfwWaitEvents();
}
WindowsでOpenGLES --- ANGLE準備
WindowsでOpenGLESを使いたい場合は、Google開発のANGLEが利用できます。
GitHub - google/angle: A conformant OpenGL ES implementation for Windows, Mac and Linux.
ANGLEを利用すれば、OpenGLESで記述したものを、Direct3DやDesktop OpenGLでレンダリングしてくれます。(現在デフォルトでDirect3D11を利用。eglGetPlatformDisplayEXTで変更可能)
今回はANGLEをビルドして、動作確認をしてみたいと思います。
ビルド
WindowsでビルドするにはGypを利用する必要がありますが、Microsoftが下記のリポジトリを作成してくれているので、こちらを利用します。
GitHub - Microsoft/angle: ANGLE: OpenGL ES to DirectX translation for Windows Store, Windows Phone and Desktop
ダウンロードしたら、src/angle.slnを開いてビルドします。
gypフォルダの下にビルド結果が配置されます。
基本的に利用するのはlibEGL.dllとlibGLESv2.dllの2つです。
ウィンドウ準備
今回はWin32APIでウィンドウを作成します。
ちなみにANGLEではウィンドウ(HWND)を準備することなくオフスクリーンレンダリングすることも可能です。
WNDCLASSEX wndclass; wndclass.cbSize = sizeof(WNDCLASSEX); wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = GetModuleHandle(NULL); wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = NULL; wndclass.lpszMenuName = NULL; wndclass.lpszClassName = L"GL Window"; wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); RegisterClassEx(&wndclass); HWND hwnd = CreateWindow( L"GL Window", L"GL Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wndclass.hInstance, NULL); ShowWindow(hwnd, SW_SHOWNORMAL);
WndProc
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); }
OpenGLES初期化
display、config、contextの順に作成し、最後にsurfaceを作成しMakeCurrentを行います。
今回はES3.0を利用するよう初期化しています。
hwndは上記で作成したものを利用します。
#include <EGL\egl.h> #pragma comment(lib, "libEGL.lib") // display EGLDisplay display = eglGetDisplay(GetDC(hwnd)); assert(display); EGLint major, minor; assert(eglInitialize(display, &major, &minor)); assert(eglBindAPI(EGL_OPENGL_ES_API)); // config EGLint configAttributes[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_BUFFER_SIZE, 32, EGL_NONE }; EGLConfig config; EGLint num_config; assert(eglChooseConfig(display, configAttributes, &config, 1, &num_config)); // context EGLint contextAttributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; EGLContext context = eglCreateContext(display, config, NULL, contextAttributes); assert(context); // surface EGLSurface surface = eglCreateWindowSurface(display, config, hwnd, NULL); assert(surface); assert(eglMakeCurrent(display, surface, surface, context));
描画
WndProcのWM_PAINTに描画処理を追加します。
今回は背景を灰色に塗るだけです。
#define GL_GLEXT_PROTOTYPES #include <GLES3\gl3.h> #pragma comment(lib, "libGLESv2.lib") case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); glClearColor(0.5, 0.5, 0.5, 1); glClear(GL_COLOR_BUFFER_BIT); assert(eglSwapBuffers(display, surface)); EndPaint(hwnd, &ps); }
Run
メッセージループを開始します。
灰色に塗りつぶされたウィンドウが表示されます。
MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }