おもちゃバコ

中身スカスカ♡

C++: 標準出力に色をつけてみた

こんちは

C++で標準出力(std::cout)に色を付けた時の備忘録です。


何が出来るの?

標準出力で色付き画像の表示ができるようになります。

つまり

  • OpenGLDirectXなどの代表的なグラフィックライブラリは不要
  • 標準ライブラリのみで描画が可能
  • わりと簡単

なので,CUI環境でも画像処理や3DCGが捗るよ!!!

あと,簡単に画像を確認したいときに単色よりは役立つよね。


環境

2つの環境で開発し,実行結果を確認しました。

OS

言語

開発環境1

開発環境2

  • WSL2 Arch Linux
  • g++ 11.1.0

参考文献

こちらの記事を参考にさせて頂きました。

en.wikipedia.org
www-he.scphys.kyoto-u.ac.jp
nodachisoft.com
daeudaeu.com


原理

察しが付くと思いますが「エスケープシーケンス」を利用しただけです。

例えば [0, 255] のRGB色の文字を表示する場合のエスケープシーケンスは

\x1b[38;2;<R>;<G>;<B>m

です。

雑な解説

「\x1b」は16進数のASCIIコードでESC(エスケープ)を意味します。
その次の「[38」は文字のカラーコード指定,「2;」が[0,255]のRGB指定です。
その後に「R;G;B」で[0,255]を指定し,「m」でエスケープシーケンスを締めます。

つまり

std::cout << "\x1b[38;2;10;20;30m" << "A I U E O" << std::endl;

は「A I U E O」をR=10, G=20, B=30の色の文字で表示しろという命令となります。

文字ではなくて背景色を変更したい場合は「48」を指定します。
表示行の色が変化するため改行前に「\x1b[m」で元に戻すことをおススメします。

std::cout << "\x1b[48;2;10;20;30m" << "A I U E O" << "\x1b[m" << std::endl;

画面クリアの実装はカーソル移動のエスケープシーケンスを使用します。

\x1b[K      // カーソル位置から右側を消去 
\x1b[<num>A // カーソルを<num>行移動  

表示した分だけ上って削除を繰り返せば良いですね。
雑に実装するとこんな感じ。

for (uint32_t i = 0u; i < height; ++i)
{
  for (uint32_t j = 0u; j < width; ++j)
  {
    std::cout << "\x1b[1A" << "\x1b[K";
  }
}

実装

エスケープシーケンスの使い方さえわかれば実装は簡単ですね。
C++で実装しましたが,CでもPythonでも何でも大丈夫なはずです。

自分は配列にRGB値を押し込んで表示するようにしました。
最も簡単な方法だと思います。

const uint32_t width = 200u;
const uint32_t height = 100u;
uint32_t* pImage = new uint32_t[width * height];

// x, yが座標,valに画素値が入っているとする
// uint32_t val = (b << 16) | (g << 8) | (r << 0);
pImage[y * width + x] = val;

何となく表示速度が問題になりそうな気がしたので, 一度文字列として表示する情報を構築してから最後にstd::cout に流し込むようにしました。

フラッシュの速度などは計測していませんが,逐次フラッシュをかけるよりは速いと思っています。

標準出力に結果を表示する際は更新速度が速すぎると表示がおかしくなるので, 表示完了後から何秒か待ったほうが良いです。
ダブルバッファに対応したり,描画と処理のスレッドを分けたりするといい感じになると思いました。

自分の実装はこちら。
github.com

Windowsの人向け

Visual StudioC++コンパイラを利用している時に, エスケープシーケンスが上手く処理されない場合がありました。

そんな時はWindows.hの「ENABLE_VIRTUAL_TERMINAL_PROCESSING」のフラグを立てましょう。

HANDLE stdHandle = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(stdHandle, &mode);
SetConsoleMode(stdHandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);

結果

PGMやPPM形式が扱いやすかったので,何年か前に1週間レイトレで作成した画像を表示してみました。
(実行するときはコンソールをめっちゃ縮小してウィンドウ最大化するのをおススメ)

実行するとわかりますが,カーソル移動のエスケープシーケンスは表示行と論理行を区別します。
よく考えると当たり前ですが,表示が上手くクリアされずに5分ぐらい悩みました。

あと,環境によっては上手くエスケープシーケンスが処理されないようです。
MSYS2のg++でコンパイルして実行したときは処理されていませんでした。
おま環な気がする(?)

200×100: 文字色(\x1b[38)

200×100: 背景色(\x1b[48)

800×400: 文字色(\x1b[38)

800×400: 背景色(\x1b[48)

1週間レイトレ(Ray Tracing in One Weekend)はこちら。
raytracing.github.io

1週間レイトレに取り組んでいる時は,標準出力をリダイレクトして作成したPPM形式のファイルをinkscapeで表示していましたが, エスケープシーケンスを利用すれば別アプリを立ち上げなくて済みますね。

カーソル移動などを工夫すればリアルタイムに更新した結果を表示出来るので楽しいかも。
(某ベンチマークソフト的な描画もできるかもね)


感想

最終的にエスケープシーケンスを利用してコンソール上に色つきの画像を表示してみました。

色分解はあまり高くなく,表示速度や疑似輪郭の問題はありますが, CUI環境でもCGで遊べる環境が作れることが分かったので良かったです。

標準機能のみで遊べるので, CUI用の自作レンダラとか作れると激アツかもね。

おわり