NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどなどをやったことを記すブログです。日本語以外の国語が混ざることがあります。

imguiで日本語文字を出力するためにOSからのフォントを読み込みしたこと。

github.com

imgui(厳密にいうとDear ImGui)というGUIライブラリーはC++環境でUI窓を作らせてくれる便利なライブラリです。QtまたはNana、Juceなどに比べればちょっと違った構造を持っていますが、プロトタイプまたは既存のビデオゲームなどでのデバッグUIなどなどによく使われるらしいです。(有名ゲームメーカーのSEGAさんのソニックゲームのエンジンにも使われるらしいです。)

f:id:neuliliilli:20190211135801p:plain
基本的にはASCII以外の文字は「?」文字で化けてしまいます。

しかし、imguiは基本的にはUnicodeの描画を実装していません。しかしUnicodeの文字に該当するglpyhを生成するAPIは存在しますので日本語、韓国語などの文字を描画するには以下のような関数を呼び出します。

// Create glyphs.
ImGuiIO& io = ImGui::GetIO()
io.Fonts->AddFontFromFileTTF(/* Font file path */, 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese());

/* Font file path */には読み込みするフォントファイルの経路が入ります。普通は直接経路を指定することで文字コードに該当するGlyphを絞り出しますが、私はOSからフォントファイルが設置しているかを確認し、そしてフォントファイルの経路を取って便利にGlyphを生成するようにしたかったんです。

ですので実装してみたところ、余計に長い道のりの末に日本語の文字を描画することになりました。そしてもしかしてまた参考にするためにこの記事をもってやり方のメモをしたいと思います。

やり方

表の世界

// Check japanese (meiryo UI) font is exist on system. 
{
  std::string japaneseFontName = "";
  if (windowManager.IsFontExistOnSystem(u8"Meiryo UI") == true) { japaneseFontName = u8"Meiryo UI"; }
  
  // If font name is found, add font file to imgui. Otherwise, just pass it.
  if (japaneseFontName.empty() == false)
  {
    // Get system font path from manager.
    const auto optPath = windowManager.GetFontPathOnSystem(japaneseFontName);
    MDY_ASSERT_FORCE(optPath.has_value() == true, "Unexpected error occurred.");
    // Create glyphs.
    io.Fonts->AddFontFromFileTTF(optPath.value().c_str(), 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese());
  }
}

「表の世界」では上のコードだけで読み込みが終わります。

  1. "Meiryo UI"というフォントがシステム(Windows)にあるかをIsFontExistOnSystemを使用して確認する。確認してあったら、フォントを読み込みすることが出来たといえる。探せなかった場合にはそのままに置く。
  2. GetFontPathOnSystemという関数を使用して、該当するフォントネームを持ったファイルの経路を取る。失敗する場合にはASSERTをする
  3. 持ってきた経路をImGuiIO::AddFontFromFileTTFを使用してGlyphを生成する。GetGlyphRangesJapaneseを使用してUnicodeでの日本語の文字コードの範囲を決める。
  4. PROFIT(終)
    f:id:neuliliilli:20190211140550p:plain
    情け無用
    そして以下のようにImGui::Textで文字列を出力しようとしたら
ImGui::Text(u8"Hello world! お早う御座います。\n夜空の星が輝く陰で悪(ワル)の笑いがこだまする\n"
u8"星から星に泣く人の涙背負って宇宙の始末!\n"
u8"銀河旋風ブライガー! お呼びとあらば即、参上!!");

次のように文字がちゃんと見えるようになります。

ここで重要なのはOSからの情報を取得する関数、IsFontExistOnSystemGetFontPathOnSystemです。ここからはWindows 7以上のOSに限定される内容となりますので、他のOSではまた別となる工夫が必要です。

裏の世界(こわい)

裏の世界では、WIN32のAPIを使用してフォントがあるかを確認します。そしてレジストリを探索して、フォントのデータベースでファイルの名前を探して完全な経路として返します。

IsFontExistOnSystem

bool DDyWindowInformationWindows::IsFontExistOnSystem(_MIN_ const std::string& iFontKey) const
{
  HDC pDC = GetDC(NULL);
  LOGFONT logFont;

  // Set font information structure.
  logFont.lfCharSet = DEFAULT_CHARSET;
  logFont.lfPitchAndFamily = 0;

  // @reference https://www.gpgstudy.com/forum/viewtopic.php?t=7047
  std::wstring fontKey;
  {
    USES_CONVERSION;
    fontKey = A2W(iFontKey.c_str());
  }
  wcscpy(logFont.lfFaceName, fontKey.c_str());

  // EnumFontFamiliesEXW (Multi-byte dirty function) returns `0` when found given font face-name.
  const auto flag = EnumFontFamiliesExW(pDC, &logFont, EnumFontFamExProc, reinterpret_cast<LPARAM>(&fontKey), 0);
  return flag == 0;
}
int CALLBACK EnumFontFamExProc(
    _MIN_ const LOGFONT* lpelfe, _MIN_ const TEXTMETRIC* lpntme, 
    _MIN_ DWORD FontType, _MIN_ LPARAM lparam)
{
  std::wstring* ptrTargetFaceName = reinterpret_cast<std::wstring*>(lparam); 
  if (*ptrTargetFaceName == lpelfe->lfFaceName) { return 0; } else { return 1; }
}

EnumFontFamiliesExWでは、LOGFONT構造体を引数として情報に当てはまるフォントのLOGFONT構造体を、EnumFontFamExProcというコールバック関数を呼んで何かを処理するようにしてくれます。(コールバック関数は直接作成しなければなりません。)コールバック関数は返す値が0だったらイテレーションを停止して関数の呼び出しを終了します。0じゃない場合には最後のフォント情報までコールバックを繰り返します。

ちょっと面倒くさいことが、WIN32ではWCHAR(マルチバイト文字)を使いますので、std::stringcharなどを使う場合には文字列をstd::wstringなどに変換しなければなりません。ここで15年前の小テクニックを使用して簡単にwstringに変換します。

// @reference https://www.gpgstudy.com/forum/viewtopic.php?t=7047
std::wstring fontKey;
{
  USES_CONVERSION;
  fontKey = A2W(iFontKey.c_str()); // iFontKey is std::string.
}

小テクニックを使うにはヘッダーファイル#include <atlconv.h>が必要となります。

GetFontPathOnSystem

ここではRegOpenKeyEx RegQueryInfoKey RegEnumValueなどを使用して、レジストリのデータベースを探索します。フォントのテーブルがあるところはSoftware\\Microsoft\\Windows NT\\CurrentVersion\\Fontsになります。

std::optional<std::string> DDyWindowInformationWindows::GetFontPathOnSystem(_MIN_ const std::string& iFontKey) const
{
  // Check validity.
  if (this->IsFontExistOnSystem(iFontKey) == false) { return std::nullopt; }
  
  // @reference https://www.gpgstudy.com/forum/viewtopic.php?t=7047
  std::wstring fontFacename;
  {
    USES_CONVERSION;
    fontFacename = A2W(iFontKey.c_str());
  }

  // Find path using registry.
  static const auto fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
  HKEY hKey;

  { // Open Windows font registry key
    const auto result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, fontRegistryPath, 0, KEY_READ, &hKey);
    if (result != ERROR_SUCCESS) { return std::nullopt; }
  }

  DWORD maxValueNameSize, maxValueDataSize;
  {
    const auto result = RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, 0, &maxValueNameSize, &maxValueDataSize, 0, 0);
    if (result != ERROR_SUCCESS) { return std::nullopt; }
  }

  DWORD valueIndex = 0;
  LPWSTR valueName = new WCHAR[maxValueNameSize];
  LPBYTE valueData = new BYTE[maxValueDataSize];
  DWORD valueNameSize, valueDataSize, valueType;
  std::wstring wsFontFile;

  { // Look for a matching font name
    LONG result;
    do 
    {
      wsFontFile.clear();
      valueDataSize = maxValueDataSize;
      valueNameSize = maxValueNameSize;

      result = RegEnumValue(hKey, valueIndex, valueName, &valueNameSize, 0, &valueType, valueData, &valueDataSize);
      valueIndex++;

      if (result != ERROR_SUCCESS || valueType != REG_SZ) { continue; }
      // Find a match. 
      std::wstring wsValueName(valueName, valueNameSize);
      if (wsValueName.find(fontFacename) != std::wstring::npos)
      { wsFontFile.assign((LPWSTR)valueData, valueDataSize); break; }
    }
    while (result != ERROR_NO_MORE_ITEMS);
  }

  // Release temporary chunk.
  delete[] valueName; delete[] valueData;

  // Close registry.
  RegCloseKey(hKey);
  if (wsFontFile.empty()) { return std::nullopt; }

  // Build full font file path
  WCHAR winDir[MAX_PATH];
  GetWindowsDirectory(winDir, MAX_PATH);

  std::wstringstream ss; ss << winDir << "\\Fonts\\" << wsFontFile;
  wsFontFile = ss.str();

  return std::string(wsFontFile.begin(), wsFontFile.end());
}

参照

stackoverflow.com

www.gpgstudy.com

Windows+VS環境でabort()をハンドルする方法

やり方

Windowsでのabort()は基本的にはSIGABRTというシグナルを出します。 -NIX系列のOSではsigactionなどの関数でシグナルをハンドリングすることが出来ますが、Windowsのデバッグ環境で呼び出されるabort()から出るシグナルをハンドリングは以下のようなコールバックを指定することで出来ます。

void __SignalHandler(int signal)  
{  
  if (signal == SIGABRT) { exit(3); }
}

そしてこの関数を以下のコードとして指定します。

typedef void (*SignalHandlerPointer)(int);  
SignalHandlerPointer previousHandler = signal(SIGABRT, __SignalHandler); 

コールバック関数の引数であるsignalはインタラプトされた信号の番号を渡します。上記のコードではSIGABRTしか処理しませんでしたが、参考にあるリンク先であらゆるWindowsのシグナルを受け取って処理することが出来ます。

参考

docs.microsoft.com

docs.microsoft.com

nlohmann::json「JSON for Modern C++」の浮動小数点数の減らし方のメモ

はじめに

github.com

nlohmann氏のJSONライブラリである「JSON for Modern C++」はC++環境でのJSONシリアルライズ文字列を読み込みするときに有用に使えるライブラリです。 速度面としては「RapidJSON」のようなライブラリには勝てないんですが、STLの操作面をよく真似していて使いやすいです。

しかし大量の浮動小数点数をシリアルライズ化しようとすると、規約上小数点以下10単位まで書くようになっていてファイルの容量がだいぶ大きくなります。

f:id:neuliliilli:20190125014729p:plain
小数点以下が10単位となってひどくなる

ここでは「JSON for Modern C++」ライブラリを利用しながら浮動小数点の小数点以下単位を抑える方法を書きたいと思います。

やり方

  1. ライブラリのjson_fwd.hppを開ける。
f:id:neuliliilli:20190125015048p:plain
  1. 最終ラインのusing json...を以下のように書き直す。
/*!
@brief default JSON class

This type is the default specialization of the @ref basic_json class which
uses the standard template types.

@since version 1.0.0
*/
//using json = nlohmann::basic_json<>;
using json = nlohmann::basic_json<
  std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>

最後の型引数としてfloatを書くことで、浮動小数点数をfloatとして取り扱うようにします。これにより、シリアルライズする時に余計な小数点がつかないようになります。

f:id:neuliliilli:20190125015645p:plain
小数点以下の表現が縮まったのが見える

まとめ

floatを使うことと、既存のdoubleを使った時の容量の差は以下の画像で見れます。

f:id:neuliliilli:20190125015457p:plain
「크기」は「容量」です。

ShaderToyで「Nier:Automata」の後処理っぽくレンダリングしてみた。

はじめに

sawcegames.com

今回はスクエア・エニックスさんのNier Automataの様々な後処理効果をShaderToyで再現してみようかとします。上のリンクから提供するチュートリアルを見ながら、HLSLコードをShaderToyが支援するGLSLに移してみたいと思います。

続きを読む

ShaderToyで「アナログ画面」っぽくレンダリングしてみた。

はじめに

以前にやったプロジェクトの(けど結局限界性がはっきりしてて捨てざるを得なかった)OPGS16ゲームフレームワークでは、下のように昔のテレビでゲームをするようにする後処理がありました。

続きを読む