ビットマップフォントをなんとなく縁取り
お仕事でそういう需要があったのです。
縁取り…アウトラインのデータが取れるならパスに沿ってラインを描くのがよさそうなんだけど、ビットマップだと…?
たぶん、縁取りって元の文字の線を太らせたものを下に敷くのでいいのでは?
問題はどうやって文字を太らせるか?単純に考えると線のピクセルを外側に押し出してやればよさそうですけども、シェーダーで描くときなんかは描画するピクセルにはどこから押し出されてくるのか?で考える必要があります。例えば下図の赤いピクセル。
基本的な考え方としては隣接あるいは近隣のピクセルを参照してどうにかしようということになります。こういう場合はN×Nピクセル分のフィルターをつくって処理するのが定番ですね。例えば2ピクセル太らせるなら上下左右2ピクセル+自分で5×5のフィルターになります。
このフィルターの中で一番グレースケールの値が大きいピクセルをとってくる、だと斜め方向に太りやすくなってしまうので形がガタガタになってしまいます。たぶん。
なのでまずは「描画するピクセルを中心とした円に含まれる各ピクセルの面積の割合をグレースケール値に掛ける」という方法を試してみました。面積の割合は、難しい計算をするのは大変なので「各ピクセルを4x4のサブピクセルに分割してフィルターの中心からの距離が円の半径以内のピクセルを数える」という素直な方法にしました。
角のピクセルは16のうち3つのサブピクセルが半径以内なので3/16=0.1875となります。思ったより完全に円に含まれるピクセルが多い…
このフィルターを用いた太らせフォントの各ピクセルの計算は「参照ピクセルのグレースケール値×フィルターの値」の最大値を採用する、となります。これをもともとの描画ピクセルのグレースケール値とアルファブレンディングします。
Color Blend(Bitmap bmp, int x, int y, float[,] filter, Color outline) { float pix = 0; for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j) { Color p = bmp.GetPixel(x + i - N / 2, y + j < N / 2); float f = p.R * filter[i, j]; if (p > pix) { pix = p; } } } float a = bmp.GetPixel(x, y).R / 255.0f; int r = (int)(a * 255 + (1 - a) * outline.R * pix); int g = (int)(a * 255 + (1 - a) * outline.G * pix); int b = (int)(a * 255 + (1 - a) * outline.B * pix); return Color.FromArgb(r, g, b); }
そんな処理を施した結果がこちら。
右側はフィルターを可視化したもの。そこそこいい感じになったと思いますが元のフォントと縁取りの境目部分がすこしガタガタしているのが気になります。これは元々のビットマップの時点でガタガタしてたのである程度は仕方ないです。もう一つ気になるのは文字の払いの部分が丸まってしまっているところ。これはまあるいフィルターで計算してるからですね。これらは今後の課題です。