円の描画

前回の線分生成と同様な手法で, 円の走査変換をプログラミングしてみよう.

今回も MathJax を利用させていただいております.

問題の設定(円の描画)

次のような設定のもとで, Fig.1 のような円周を描くことにする:

Fig.1. 円

低速アルゴリズム(直接解法)

もっとも単純な 1/8 円弧の描画アルゴリズムは次の通り:

この方法では,実数演算(平方根)と乗算(自乗)があるので, 処理速度的に不利である.

他の単純な方法としては,多角形近似がある. 短い線分を組み合わせて,円を表現する. しかし,頂点座標を三角関数(sin,cos)によって算出する必要があり, やはり,処理速度の問題がある. 三角関数のルックアップテーブル(事前に算出しておいた数値配列) を利用する方法もあるが,それでも実数の乗算を伴う.

なお,プログラムを実装する際には,前回の線分と同様, 簡単化・効率化のために, \( x_0 = 0 \) ,\( y_0 = 0 \) とみなして計算し, 画素 \( \P({x_0 + x,\ y_0 + y}) \) を点灯する. また,この場合には,円の対称性より, 次のような8個の画素を点灯すれば 0 〜 360度の円が完成することになる:

ちなみに,\( y \) 座標から \( x \) 座標を求めるための数式のC言語コードは:

x = (int)round(sqrt((double)(r*r - y*y)));	// round() は四捨五入の関数

低速アルゴリズムでは,この式を利用することになる.

または:(低速版の多少の高速化)

x = (int)(sqrt((double)(r*r - y*y)) + 0.5);

両者は同じような計算なのに,なぜ処理速度が変わるのか? 計算方法に多少の違いがある他, 関数の呼出や戻り値の返却の処理にも時間を要するので... 一般に,ループ内での関数呼出は処理速度低下を招く.

ただし今回は,低速版と高速版とを比較したいので, 前者の関数 round() を使い,差を明確化しておきましょう.

また,本来なら関数 Plot() も(さらに,その実体である mvaddch() も) ループ内では使いたくないところだが, 描画できなくなるのも困るので, これは低速版・高速版ともに気にしないでおこう.

関数 Plot() の8回呼出のコストを軽減するには, こんな関数を利用するとよいだろう:

void Plot8(int x0, int y0, int x, int y, char c)
{
	if (!target) return;

	mvaddch(y0-y, x0-x, c);
	mvaddch(y0-y, x0+x, c);
	mvaddch(y0+y, x0-x, c);
	mvaddch(y0+y, x0+x, c);

	mvaddch(y0-x, x0-y, c);
	mvaddch(y0-x, x0+y, c);
	mvaddch(y0+x, x0-y, c);
	mvaddch(y0+x, x0+y, c);
}

高速アルゴリズム(インクリメンタル解法)

前回紹介したブレゼンハムのアルゴリズムを応用すれば, 直線だけでなく,円についても, 整数のインクリメンタル計算による高速描画が可能である.

円の方程式より次の誤差関数が得られる: (定義式

\[ f(x,\ y) = 4 \P\{{\P({x - x_0})^2 + \P({y - y_0})^2 - r^2}\} \]
なぜ4倍?前回は2倍だったなー.類推!!

この関数の値は:

アルゴリズムについては,Fig.2 の説明図および 前回の高速アルゴリズム (ブレゼンハムの線分生成アルゴリズム) を参考にして考えてみよう.

Fig.2. ブレゼンハムのアルゴリズムによる円周の描画

なお,アルゴリズム中で利用される計算式は次の通り: (漸化式

これで,元々は2乗を含んでいた誤差関数を, 簡単な整数の足し算とシフト演算( \( 2^n \) 倍)だけで計算できるようになった.

前回同様,コンピュータには,定義式では計算させない. 漸化式で計算させる.

右辺の関数値 \( f \) の部分には, 既に計算済みの変数値を使う. ループ内で関数 \( f \) を定義式通りに何度も再計算してしまうのは非効率.

なお,このアルゴリズムでも,実装上は, \( x_0 = y_0 = 0 \) として計算し, 画素 \( \P({x_0 + x,\ y_0 + y}) \) 等を点灯する方が簡単.


本日の課題

前回作成したプログラム line.cを元にして, ディジタル円を生成するための関数の低速版 SlowDrawCircle() および高速版 FastDrawCircle() を定義し, 実行速度を比較せよ:

void SlowDrawCircle(int x0, int y0, int r, char c)
{
	...
}

void FastDrawCircle(int x0, int y0, int r, char c)
{
	...
}

高速版では,無駄な計算を極力排除するよう,工夫すること. 前回の注意事項も参考にしよう.

まずは,円を正しく描画できることを確認すること. その後で速度計測だ.

前回の線分描画では効果を実感できなかったかもしれないが, 今回はかなりの高速化が期待できるだろう.

上級者向け追加課題

(レポートに説明があれば加点対象.)

レポート提出

おまけ

円と三角形の塗り潰しのアルゴリズムについて, 参考資料(2007年度の課題) に説明がある.