NanoVG のソースコードを読んでみた

NanoVG
https://github.com/memononen/nanovg
HTML5Canvas API ライクなベクタグラフィックスライブラリ。

ベクタグラフィックスかっけぇ!どうやってるの?」と思ったのがひとつ。
「今作ってるゲームライブラリにはまともな図形描画機能が無い」と思ったのがもうひとつ。
自前で実装するか、外部ライブラリを組み込むか。

基本的な流れ

① nvgBeginFrame() で一連の描画を開始する。
② nvgBeginPath() で図形ひとつ分の描画を開始する。
③ nvgMoveTo(), nvgLineTo(), nvgRect(), nvgCircle() 等で図形を作る。
④ nvgFillPaint() または nvgStrokeColor() 等でで塗りつぶし方法を指定する。
⑤ nvgFill() または nvgStroke() で描画を行う。
⑥ nvgEndFrame() で一連の描画を終了する。

少し細かく。
② はキャッシュのクリア。一連の処理の中ではメモリ使用量を計っていて、
メモリが足りなくなるたびに realloc() している。そのメモリ使用量を 0 にしている。
ちなみにキャッシュ本体は NVGpathCache。

③ は描画コマンドの生成。nanovg はすぐに描画を行うのではなく、
図形生成に関する処理は先ず一度コマンド化される。
コマンド種別(enum) とパラメータ(座標等) が並ぶ float 配列。

④ は次の描画で使う NVGpaint 構造体の指定。いわゆる「ブラシ」に相当する。
NVGpaint は nvgLinearGradient() 等で生成する。(構造体の初期化だけで malloc とかしないので割とガンガン作ってもOK)

⑤ はプリミティブの構築と、実際の描画を行う。
かなり複雑なので後述。

描画コマンドの作成

こんなかんじ。

コマンドの配列 ・・・ NVGcontext::commands
コマンドの数  ・・・ NVGcontext::ncommands

↓例えば、nvgRoundedRect() が作るコマンドはこう。

float vals[] = {
	NVG_MOVETO, x, y+ry,	// 左上
	NVG_LINETO, x, y+h-ry,	// 下へ
	NVG_BEZIERTO, x, y+h-ry*(1-NVG_KAPPA90), x+rx*(1-NVG_KAPPA90), y+h, x+rx, y+h,	// 左下のカーブ
	NVG_LINETO, x+w-rx, y+h,
	NVG_BEZIERTO, x+w-rx*(1-NVG_KAPPA90), y+h, x+w, y+h-ry*(1-NVG_KAPPA90), x+w, y+h-ry,
	NVG_LINETO, x+w, y+ry,
	NVG_BEZIERTO, x+w, y+ry*(1-NVG_KAPPA90), x+w-rx*(1-NVG_KAPPA90), y, x+w-rx, y,
	NVG_LINETO, x+rx, y,
	NVG_BEZIERTO, x+rx*(1-NVG_KAPPA90), y, x, y+ry*(1-NVG_KAPPA90), x, y+ry,
	NVG_CLOSE
};

コマンドの追加は nvg__appendCommands() で。
メモリが不足していれば realloc する。

プリミティブの構築と描画

重要なのは以下の関数たち。
nvg__flattenPaths()
nvg__tesselateBezier()
nvg__calculateJoins()
nvg__expandFill()
glnvg__renderFill()

・nvg__flattenPaths()
これは描画コマンドを解析して頂点配列を生成する。
dpiを考慮したベジェ線の生成もここ。
この時点ではまだ線分の太さは考慮していない。
点情報は NVGpoint 構造体で、頂点配列はキャッシュに生成される。
また、ひとつ前の点からの相対座標 NVGpoint::dx,dy も計算している。

・nvg__tesselateBezier()
ベジェ線の生成。nvg__flattenPaths() から呼ばれる。
カーブを構成する 4点を受け取り、間が十分に小さくなるまで再帰で点を作っている。
アルゴリズムまでじっくり見てないけど、入力が点4つだから Catmull-Rom っぽい?
長い曲線を描画しようとすると計算量が跳ね上がりそう。

・nvg__calculateJoins()
各点に対して、ひとつ前の点との位置関係から「押し出し方向 (NVGpoint::dmx,dmy)」を求めている。
これは向きベクトルで、線の太さを乗算すると最終的な頂点位置が求まる仕組み。

・nvg__expandFill()
ここまでで生成した情報を元に、最終的な頂点配列を生成する。
この頂点配列は OpenGL の頂点バッファとして描画される。
また、アンチエイリアス用の Stroke も作っている。
これは NVGparams::edgeAntiAlias が 1 のときに行われる処理で、
詳しくは読んでいないがおそらく透明度を落とした細い Stroke を図形の外周に沿って生成しているのだと思う。

・glnvg__renderFill() (OpenGL ドライバ)
必要なデータは既にそろっているので、頂点バッファや
シェーダの Uniform 変数にセットして描画するだけ。

塗りつぶしについて

nvgLinearGradient() や nvgRadialGradient() で作られる NVGpaint なナニモノなのか。

最初全然わからなかったけど、どうやらこれはフラグメントシェーダにてあるピクセルを innerColor と outerColor の色座標系(いい言葉が思いつかないけど) にトランスフォームする座標変換行列(+補助情報) みたいです。

シェーダのコードは nanovg_gl2.h の fillFragShader 変数に入ってるのが読みやすいかも。
ピクセル座標を 0.0 ~ 1.0 に変換して、
innerColor ~ outerColor を線形補間してる。

NanoVG でできないこと

一度の nvgFill() では、WPF の GradientStop みたいに複数の色でグラデーションを作るのは無理みたい。
https://msdn.microsoft.com/ja-jp/library/system.windows.media.lineargradientbrush%28v=vs.110%29.aspx
複数回に分けて描画することになる。

DirectX で動かすには

必要なドライバ関数は NVGparams 構造体にまとまっている。
これらを DirectX 用に実装することになるが、そんなに数は多くない。

struct NVGparams {
	int (*renderCreate)(void* uptr);
	int (*renderCreateTexture)(void* uptr, int type, int w, int h, int imageFlags, const unsigned char* data);
	int (*renderDeleteTexture)(void* uptr, int image);
	int (*renderUpdateTexture)(void* uptr, int image, int x, int y, int w, int h, const unsigned char* data);
	int (*renderGetTextureSize)(void* uptr, int image, int* w, int* h);
	void (*renderViewport)(void* uptr, int width, int height);
	void (*renderCancel)(void* uptr);
	void (*renderFlush)(void* uptr);
	void (*renderFill)(void* uptr, NVGpaint* paint, NVGscissor* scissor, float fringe, const float* bounds, const NVGpath* paths, int npaths);
	void (*renderStroke)(void* uptr, NVGpaint* paint, NVGscissor* scissor, float fringe, float strokeWidth, const NVGpath* paths, int npaths);
	void (*renderTriangles)(void* uptr, NVGpaint* paint, NVGscissor* scissor, const NVGvertex* verts, int nverts);
	void (*renderDelete)(void* uptr);
};

まとめ

曲線のテッセレーションストロークアルゴリズムは非常に参考になった。
頑張れば自分でも実装できそうな規模。

nanovg を Lumino (作成中のゲームエンジン) に組み込むのはアリだけど、
フルに機能を使うかは直近ではちょっと微妙。
描画スレッドを実装する Lumino には nanovg 全体をラップするクラスが必要そう。

ベジェ線がどうしても必要にならない限りは見送りかな・・・。
ヒントはすごいもらったからソレはソレで早速活用したいところ。