閉領域内部の塗りつぶし方 + 曲線の描画法(ラスタライズ)


  

ここでは、菊の葉を描くことを通して、線分で囲まれた閉領域の内部を塗りつぶす方法について考える。 そのための手法を学ぶために、計算幾何学の基礎の基礎についても触れる。

  1. 菊の葉は結構ギザギザがあるので、まず思いつくのは ギザギザの頂点の座標を線で結ぶ方法 (図1)である。

  2. 問題は内部を塗り潰す方法である。これまで、いろんなものの内部を塗り潰すのに用いた「輪郭を表す閉曲線をスケールをだんだん小さくしながら描く」という手法は、ここ で述べているような問題がある。実際、この方法で描くと(本当は 図3 のように描きたいのに) 図5 のようになってしまう。その理由は、図4 の緑の線と赤い線を見てみればわかる。赤い線(scale=2.5)はスケールを緑の線(scale=5.0)の半分にして描いたものであるが、緑の線と交差していることからわかるように、葉の外側も塗り潰してしまうのである。これは、葉の下部(葉柄の下部)を中心として、葉の縁を表す曲線上の点 (x,y) をスケーリングするために起こる。
     これを無くすには、スケーリングする際の中心の x 座標を葉の下部(葉柄末端)の x 座標と上端の x 座標の中点にすればよい。そうすると、scale を変えたときの曲線は 図2 のようになり、この方法で scale を 0 になるまで徐々に小さくして曲線を描くと 図3 のように、内部をきちんと塗り潰すことができる。
    ソースコード
    図1  図2  図3 
  3. しかし、実際に実行してみると分かるが、図3を描くのは非常に時間がかかる。その理由は、scale を小さくするのを粗くすると 図6(scale -= 0.05) のように描き残しが生じてしまうために、scale を少しずつ小さくしなければならない (scale -= 0.001 の例) ためである。
    図4  図5  図 6 
  4. しかし、この方法でも、葉の縁を表す多角形が凸多角形でない場合(例えば、このように 図7 凸多角形でない場合にはうまくいかない。実際、塗り潰しを行なってみると 図8(描いている途中) のように scale が小さくなっていく過程で、葉の外部である凹の部分も塗り潰されて 図9(描き終わったとき) のようになってしまう。
    図7  図8  図9 
  5. ところで、上述のように葉の縁を表す多角形に凹んだ部分がない場合(すなわち、凸多角形の場合)には、もっと高速に内部を塗り潰すことができる。 それは、縁を表す曲線が凸多角形の場合には曲線上の点から x 軸へ下ろした垂線が曲線の他の部分と交わらないので、曲線状の各点から x 軸へ垂線を描けばよいからである(図10 :xx を 1.0 幅で刻んで描いたもの)。この方法は、スケールを小さくしてゆくよりも簡潔で(回転の中心を考慮する必要もない)しかもスケールの縮小幅が粗いと点を描き残してしまうということもなく、高速に描くことができる。 山並 を描いたときに用いた方法(ソースコード)は、これの特別な場合である。
    図10 

  6. ここで、曲線(直線)の描画について、補足しておきたい。
     図3では線分を描画する際、反復1回当たりの xx のマイナス増分(xx を小さくする量)があまり粗いと点が描き残されてしまうのでかなり小さめにした(xx -= 0.001)。しかし、前述したように、これは描画速度を大きく落とすもとになる。そこですぐに考えつくのが、直線上の点(に対応するピクセル)だけでなく、その近辺の点(ピクセル)も同時に塗りつぶしたら(色を付けて表示したら)どうか、というアイデアである。
     実際、これを行なってみたのが、これ である(ソースコード)。点 (x,y) を描く際に、(x,y) のΔ近傍に入る点 (x±Δ, y±Δ)すべてを同時に描く。その代わり、x の増分をΔ倍する。

  7. 実は、このような考え方は、曲線(直線)を描画する際に用いられている「ラスタライズ(rasterize)」という手法に相通じるところがある。直線 y = x, y = k, x = k (k は定数) 等を描く際には、この直線上の点がかすめるピクセル(直線状の点として光らせるピクセル)は1つだけに決まる(図@)が、傾きがこれらと違う直線(例えば y = x/2)では、直線がかすめるピクセルが複数になるので、そういったピクセルすべてを光らせるべき(図A)なのか、それとも例えば図Bのように選んだピクセルだけを光らせれば十分なのかを適当な方法で決める必要が生じる。そのようなことをラスタライズという。 ラスタライズのアルゴリズムはいくつも知られているが、例えば、2点 (x1, y1), (x2, y2) が与えられたとき、x 軸方向の進む量 |x2-x1| と y 軸方向の進む量 |y2-y1| を比べて、大きい方を1ピクセルずつ進め、そうでない方は直線の傾きに応じて少しずつ進めるという方法が考えられる(ブレゼンハム (J.E.Bresenham, 1965) のアルゴリズムという)。
    ラスタライズについては、ここではこれ以上述べない(曲線の描画の際にはそういうことも考慮する必要があり、実際、たいていの描画システムではそのことを考慮した(ソフト的にあるいはハード的に)描画アルゴリズムが用いられていることを知っておけば十分である)。
    図@ 図A
    図B
  8. 話を閉領域内部の塗りつぶしに戻そう。
    これまで述べた方法は、葉の縁をなす図形が凸多角形の場合にしか適用できないので、そうでない場合(凸多角形出ない場合や、内部に穴がある場合等)にはどうしたよいであろうか? 今度は、こういったすべての場合にも適用できる方法を考える。それは、描画対象範囲にあるスクリーン上の全ての点について、それが線分で囲まれた図形の内部か外部かを判定して、内部の点だけを塗るという方法である。図形が線分だけで囲まれている場合には、どのような図形であろうともこの方法が適用できる。
     まず、準備として、ここではベクトルの内積や外積を用いて、直線と点との関係を判定する方法について述べる。

     3次元ユークリッド空間におけるベクトル a = (a1, a2, a3), b = (b1, b2, b3) を考える。ab内積 ab

        ab = a1b1 + a2b2 + a3b3
    
    により定義される実数である。 ベクトル ab のなす角度をθとすると、
        ab = |a||b|cosθ
    
    である (|・| はベクトルの絶対値(大きさ))。下図参照。
    一方、外積 a×b
        a×b = (a2b3 - a3b2, a3b1 - a1b3, a1b2 - a2b1) 
    
    により定義されるベクトルであるa×b の絶対値 |a×b| は
        |a×b| = |a||b|sinθ
    
    で与えられ、その向きは a, b を含む平面に垂直で、a から b へ右ねじの進む向きになる(右手の親指を a、人差し指 b としたとき、a から b の方向へ右巻きのネジを回したとき(つまり、右巻きのネジを左回しして抜くとき)にネジが進む方向が a×b の向きである。下左図参照)。
     定義より、外積の大きさはベクトル a, b が成す平行四辺形の面積になる(下左図参照)。面積と言えば正の実数であるが、ここでは sinθ の値に従って値が負となる場合も考える(符号付き面積)。
     平面上で3頂点 A(ax,ay), B(bx,by), C(cx,cy) が与えられているとき(下右図)、三角形 ABC の面積は
         Δ(A,B,C) := ((bx - ax)(cy - ay) - (cx - ax)(by - ay)) / 2 
    
    として求めることができるので、下左図の平行四辺形の面積は a(a1,a2), b(b1,b2) の座標を用いて
        2Δ(A,B,C) = (a1 - 0)(b2 - 0) - (b1 - 0)(a2 - 0) = a1b2 - b1a2 (|a×b| に等しい)
    
    と表すことができ、これは |a×b| に等しい。
  9. さて、多角形の内部を塗りつぶすために、7. で述べた外積(符号付き面積)を使うことができる。上左図からわかるように、ベクトル b がベクトル a の上側にあるとき(0<θ<π のとき)に上左図の平行四辺形の面積は正となり、下側にあるとき(-π<θ<0)に負となる。よって、点 C が線分 AB の上側にあるか下側にあるかは、ベクトル AB = (b1-a1, b2-a2, b3-a3) とベクトル AC = (c1-a1, c2-a2, c3-a3) の外積 Δ(A,B,C) を計算してその符号の正負を調べればよい。Δ>0 なら C は AB の上側にあり、Δ<0 なら C は AB の下側にある。
  10. 内積と外積に関してもう一つ重要なことは、2つのベクトルが直交しているかや平行であるかの判定にも使えることである。
    内積は cos(・) の、外積の絶対値は sin(・) の定数倍になっているので、2つのベクトルが直交しているときに内積は0になり、外積は2つのベクトルが併行のときに絶対値が0となるので、それぞれベクトルの直行判定、平行判定に用いることができる。 とくに、ここで重要なのは、以下に述べるように、直線にって囲まれた領域がどのような形であろうとも、グラフィック画面の各ピクセルが図形の内部にあるか外部にあるかを判定するのに、2直線の交差判定を使うからである。
     2点 P1, p2 を通り、P1 から P2 へ方向づけられた直線が2点 P3 と P4 を結ぶ線分と交差するのは、P1, P2 を通る直線に関して P3 と P4 が反対側にある場合である。したがって、三角形 P1P2P3 の符号付き面積(外積 Δ(P1,P2,P3))を用いると、この条件は
        Δ(P1,P2,P3)Δ(P1,P2,P4) < 0
    
    と表すことができる。 点 P1 と P2 も直線 P3P4 に関して反対側にあるなら、線分 P1P2 は直線 P3P4 と交差する。よって、2つの線分 P1P2 と P3P4 が交差する必要十分条件
        Δ(P1,P2,P3)Δ(P1,P2,P4) < 0 かつ Δ(P3,P4,P1)Δ(P3,P4,P2) < 0  
    
    である。下図参照。

  11. 上記のことを用いると、三角形の内部と外部の判定ができる。三角形 P1P2P3 の頂点は反時計回りに方向つけられているものとする。このとき、点 P(x,y) が三角形 P1P2P3 の内部にあるなら三角形のどの辺に関してもその左側になければならない。すなわち、P が三角形 P1P2P3 の内部にあるための必要十分条件は
        条件 ◇(P1,P2,P3) :  Δ(P1,P2,P) > 0 かつ Δ(P2,P3,P) > 0 かつ Δ(P3,P1,P) > 0 (*)
    
    が成り立つことである。
     したがって、点 P が凸多角形 P1P2・・・Pn の内部にあるための必要十分条件
        ある 0≦i,j,k≦n (i,j,k は互いに異なる整数)に対して ◇(Pi,Pj,Pk) が成り立つ (**)
    
    が成り立つことである。この方法は Θ(n3) 時間アルゴリズム(n3 に比例する時間がかかるアルゴリズム)である。

  12. 上述のことを使って線分で囲まれた領域の内部を塗りつぶしてみよう。まず、領域が凸多角形で、かつ領域内部に穴が無い場合には、上記の条件(**)をチェックすればよい。

     実行例  (ソースコード) 凸多角形に限定されるため、データを変えている

  13. 一方、下図のように、凸多角形でなく領域内部に穴が何重にもある場合、点 P (赤い点)が領域内部にある条件は、明らかに外部にある点 Q (黒い点)と結んだ直線が、縁をなす線分と偶数回交わることである。 ただし、もし、線分 PQ が多角形の頂点(の近辺)を通ると、その点を端点として共有する2つの線分(縁を表す線分)それぞれに対して交差回数がカウントされてしまうので、注意が必要である。
    このことに基づき、塗り潰したのが以下のものである。

  14. 領域の塗り潰し方については、ここ も参考にされたい。

  15. 以上、閉領域内部の塗り潰し方に関する数学的な考え方を少しだけ述べた。ここで述べたことはほんの入門的なことに過ぎないが、このように幾何学(図形)に関するアルゴリズムについて研究するのが計算幾何学である。ここでは方法論についてだけ述べ、アルゴリズムの効率(実行速度)のことにはまったく触れなかったが、本来の計算幾何学では、できるだけ効率の良いアルゴリズムを求めることがメインテーマでもある。
    計算幾何学の入門書はたくさん出版されているが、入門書としては例えば次のものが参考になろう: