総合科目
例題の実習およびプログラミング課題
(1)Bresenhamの直線描画 (Bresen.Java)
(2)シルピンスキーのギャスケット (gasket.Java)
(3)一価関数曲面の表示 (hidfunc.Java)
(4)凸多面体のワイヤーフレーム表示 (wire.Java)
5
.1 直線の描画ラスタスキャン方式のディスプレイでは、直線は離散的な点の集合として表現される。線分は、2端点(x1,y1),(x2,y2) の座標で定義される。この線分を描画するために直線の方程式を用いる方法もある。
y= (x-x1)*(y2-y1)/(x2-x1) + y1
この式は、2端点間にあるすべての点に対するx, y座標を求めるために使われ、xをからまで動かしてyの値を求める。ただし、x, yは整数値である。なお、走 査線の順序で線を描画する処理は走査変換(スキャンコンバージョン: scan conversion)とよばれる。また、各走査線ごとに直線の方程式を解くのではなく、1つ前の走査線での交点に増分を加える増分法が一般に用いられる。
(2)
ブレセンハム(Bresenham)のアルゴリズム線の最良な近似は、真の直線から最短距離にあるような画素を並べたものです。真の直線とそのすぐ上およびすぐ下の画素との距離をsi,tiとし、di=si-tiとすると、diの符合で真の曲線のどちらの画素を選択するかが決められ、このは簡単な加算のみで求められる。このように、加算のみで処理できる簡単方法をブレセンハムのアルゴリズムという。 効率よい計算のため整数演算で実現できるように改良されている。なお、任意の方向の直線の場合にも対応できるには場合分けが必要である。
始点(x0, y0)、終点(x1, y1)の線分を考える。なお、 説明を簡単にするため、線分が第1オクタントの場合(dx>dy>0)に限る。ただし、dx=x1-x0, dy=y1-y0. 整数表現の直線とyの真の直線のyとの差の誤差eとすると、xが1増分に対し、yを1増分させない場合の誤差は
e = e + dy/dx
となる。一方yを1増分させた場合は
e = e + dy/dx -1
となる。なお初期では誤差
eは0である。これは次のアルゴリズムで記述できる。else e= e+ dy/dx
2.2) 画素(x,y)を描画
実数演算をさけるため、誤差の式から0.5を減じ、2dxを乗じた新しい誤差dを用いると、
d = 2dy-dx
else d = d + 2dy
2.2)
始点(x0, y0)、終点(x1, y1)の線分を描画するプログラムは次のようである。なお、 説明を簡単にするため、第1オクタントの場合(dx>dy>0)に限ってある。
int x,y=y0; dx = 2*(x1-x0), dy = 2*(y1-y0) int dydx = dy - dx, d = dy - dx/2; for(x=x0; x<=x1; x++){ draw_pixel(x,y); if(d<0) d+= dy; else {y+=1; d += dydx;} |
関数
scanX を BresenDraw から呼ぶ例を以下に示している。
public void BresenDraw(int x0, int y0, int x1, int y1,Graphics g) { Int dx = x1 - x0; int dy = y1 ? y0; scanX(dx, dy, 2*dx, 2*dy, x0, y0, g); } // x 方向に走査 ; 第一オクタント(dx>dy>0)の直線のみに対応public void scanX(int dx, int dy, int dx2, int dy2, int x, int y, Graphics g) { int eps = -dx; for (int i =0; i<=dx; i++) { g.fillRect(x, y, 1,1); // 点の描画x ++; // 右へ進むeps += dy2; if (eps >= 0) { y ++; // 上に進むeps -= dx2; } } |
上記は、基本的にはx方向に一つづつ進むが、変数
epsが正になるときはyも一つ進むアルゴリズムである。このプログラム前記アルゴリズムを変更(誤差の初期値)している。他のケースの場合の含めて動作するためには、線分の傾きに応じて場合分けが必要であるので、実際のプログラムを参照のこと。3)プログラム使用法
マウスにより2端点を指定するものと、予め設定した2点を結ぶ簡易版が容易されているから、簡易版から実験をする。マウスでクリックした点は黄色の小円で表示され、2点目をクリックした際線分が表示される。さらに画面内のどこかをクリックすると画面がクリアーされ、再度線分を指定できる。
山、雲、木等も自然物を表現するにはフラクタルが有用です。フラクタルと いう言葉は、マンデルブロ(Mandelbrot)による造語です。フラクタルは、不規則な形状、自己相似性をもつ形態を表現できます。これにより、自然界の物を模擬し、自然の情景を描写することができます。例として、コッホの曲線やシルピンスキーのギャスケットがあり、相似な大小の形状が組み合わされています。
図5.1 シルピンスキーのギャスケットの分割過程
シルピンスキーのギャスケット(詰め物)は、三角形の各辺の中点を結んで三角形の中に三角形を繰り返し作成したものである(図
5.1参照)。再帰呼び出しを使って、同じ手順で三角形の中に小さな三角形を描くプログラムを作成する。以下の処理を指定回数だけ繰り返します。
1.
三角形を描く。2.
三角形の各辺の中点を結んだ三角形を描く。全体の1/4の大きさの三角形が4個できる。
3.
その中にさらに三角形を描く。
図5.2 シルピンスキーのギャスケット
プログラムの説明
(
a)繰り返し数nを設定(
b)最初の3角形の頂点座標を設定(図5.2中P0、P1、P2)。(
c) 最初の3角形を描画し、3角形を作成するメソッド(triangle)を呼ぶ。(
c)triangleでは、内側の 3角形を描画し、nが1になるまで自分自身を再帰呼び出し。(
c.1) 外側の3角形の各辺の中点座標を計算する(図中Q1、Q2、Q3)。(
c.2)内側の3角形を描く。(
c.3)3角形の頂点を置き換えて、3つの3角形(図中T1,T2,T3)の頂点を置き換えて、再帰呼び出しする。ここでの例は、、および繰り返しレベル
nを指定するすることによる再帰的描画方法とする。
1)
シルピンスキーのギャスケット;初期値設定および再帰的描画
public void paint( Graphics g ){ Dimension appsize = size(); // int w=getSize().width-10; // 横の大きさJDK 1.1int w =appsize.width-10; int [] x={ 5, w+5,w/2+5,5}; // 三角形の初期値int [] y={w+40,w+40, 40,w+40}; // 三角形の初期値 g.drawString(" step = "+n, 25,40); // n を表示 g.setColor(Color.black); g.drawPolygon(x,y,3); // 三角形の描画// 3 角形の再帰呼び出しtriangle(g,n,new Point(x[0],y[0]),new Point(x[1],y[1]),new Point(x[2],y[2])); } |
2)
三角形を再帰的に作成:
public void triangle(Graphics g,int nn, Point P0, Point P1, Point P2){ if(nn<=1){return;} int [] Qx =new int[3]; // 3角形頂点配列の生成int [] Qy =new int[3]; // 3角形頂点配列の生成 Qx[0]=(P0.x+P1.x)/2; Qy[0]=(P0.y+P1.y)/2; //新しい中点 Qx[1]=(P1.x+P2.x)/2; Qy[1]=(P1.y+P2.y)/2; // Qx[2]=(P0.x+P2.x)/2; Qy[2]=(P0.y+P2.y)/2; // g.drawPolygon(Qx,Qy,3); // 三角形の描画// P1を含む三角形を描く(再帰呼び出し) triangle(g,nn-1,new Point(Qx[0],Qy[0]),P1,new Point(Qx[1],Qy[1])); // P2 を含む三角形を描く(再帰呼び出し)triangle(g,nn-1,new Point(Qx[2],Qy[2]),new Point(Qx[1],Qy[1]),P2); // P0 を含む三角形を描く(再帰呼び出し)triangle(g,nn-1,P0,new Point(Qx[0],Qy[0]),new Point(Qx[2],Qy[2])); } |
この隠線消去はスクリーン空間で行われる。基本的な考え方は、x、y、またはzの一定値による幾つかの平行平面による曲面を考える。すなわち、幾つかの曲線の集合で表現できる。これらの切断曲線をスクリーンに投影し、2次元スクリーン上で隠線消去を行なう。その際、視点に近い断面上の曲線から順に描画する。
いま、x、y平面がスクリーンに対応し奥行きがz軸となる座標系を考える。この方法は投影後のスクリーン座標において隠線消去を行なう方法である。一価関数で表現される曲面がこの隠線消去に適している。一価関数とは、z=f(x、y)のように与えられたx、y座標に対しzは1個しか価が存在しない関数である。ここでは、y=f(x、z)の関数を考え、z = zi (I=0,1,…n)の際の曲線を描く。ここで、視点に近い順に描画する。その際描画した曲線が存在した最大値と最小値を各x座標(スクリーン座標)について記憶する。次の層の曲線を描く際記憶している最大値と最小値の範囲外なら描画し、そうでないなら描画しない。この操作を繰り返す。
計算手順は次のようである。
点P(x, y,z)を透視投影しスクリーン座標p’(x’,y’)を算出する。
3
次元座標の透視変換を行なうクラスTrans、注視点、視点を設定し、変換マトリックスの係数を計算した後に、各頂点について座標変換(または投影)を行なう。座標変換の場合は3次元座標(3次元の実数のクラスPoint3)、投影の場合は2次元(2次元の整数クラスPoint)が戻る。 なお、 視点は極座標(R、θ、Φ)で与える。マウスをドラッグして動かすと、視点が変化できるようにする。この際、マウスを左右するとθ、上下させるとΦが変化するものとする。図5.3 関数の隠線消去の例 図5.4 最大値を利用した隠線消去
曲面を上方から眺めている場合を考える。図5.4に示すように、既にPa
1、Pb1を通過する曲線が描かれているとき(その際各画素でのyの値は記憶)、Pa2,Pb2を通過する曲線を描く場合、点Pa2においては記憶されているyの値(すなわちPa1のy)より大きので可視、またPb2においては記憶されているyの値より小さいから不可視と判定される。
class Trans { Point3 ptViewRef = new Point3(0, 0, 0); // 注視点の3次元座標double theta, phi, R; // 視点位置(R、θ、Φ) double cos_theta=1., cos_phi=1., sin_theta=0., sin_phi=0.; Trans() { } public void setVRpnt( double x, double y, double z) { ptViewRef = new Point3(x,y,z); } // 注視点の設定public void setview(double r, double the, double ph) { theta=the; phi=ph; R=r; } // 視点の設定public void preper() { // 変換マトリックスの係数計算double d2r= Math.PI/180.; cos_theta =Math.cos(theta*d2r); sin_theta = Math.sin(theta*d2r); cos_phi = Math.cos(phi*d2r); sin_phi = Math.sin(phi*d2r); } public Point3 tranfrm(Point3 point) { // 透視変換(3次元à 3次元)Point3 p1 = new Point3(); Point3 p2 = new Point3(); // 平行移動p1.x = point.x ? ptViewRef.x; p1.y = point.y ? ptViewRef.y; p1.z = point.z ? ptViewRef.z; // 座標回転double xy = cos_theta*p1.x+sin_theta*p1.y; p2.z =(R-cos_phi*xy-sin_phi*p1.z)/R; p2.x = cos_theta*p1.y-sin_theta*p1.x; p2.y = cos_phi * p1.z ? sin_phi * xy; return p2; } public Point project (Point3 point) { // 投影(3次元à 2次元)Point3 p1= tranfrm(point); Return new Point((int)(p1.x/p1.z), (int)(p1.y/p1.z)); } } |
透視投影の式は資料[1]の2.6を参照のこと。
2) 3次元座標の表現
実数の3次元座標を記憶するクラスPoint3 を準備する。
class Point3 {public double x, y, z; Point3() { } Point3(double ox, double oy, double oz) { x = ox; y = oy; z = oz; } } |
3)
曲線を描画する関数
public void paint(Graphics g) { Int[ ] ymin = new int[width]; Int[ ] ymax = new int[width]; // width: 画面幅henkan.preper(); // 座標変換の係数セット for(int i=0;I<width;i++){ // ymin[],ymax[]の初期化 ymin[I]=height; ymax[I]=0;} // z,x を変化させて、y= f(z,x) を 計算for(int z=170; z>=-170; z -= 10){ for(int x=-170; x<=170; x++){ Point3 P = FuncHat(x,z); // 関数生成 P(x,y,z)Point q = henkan.project(P); // 透視投影 q(x,y)Int px = x_origin + q.x ; int py = y_origin - q.y; if( py < ymin[px] ) { ymin[px]= py; g.drawLine(px,.py, px, py); } // 可視点の描画(下側)if( py > ymax[px] ) { ymax[px]= py; g.drawLine(px, py, px, py); } // 可視点の描画(上側)} // loop for x } // loop for z } |
ここで、(
x_origin、y_origin)はスクリーンの中心座標で, width, height はスクリーンサイズである。上のプログラムは視線とZ軸とのなす角が90度以内のときのみうまくいくもので、他の方向から見る場合には、描画する曲線の順を変更する必要がある(例えば、yループの中にzループとする)。描画する点のスクリーン座標(px,py)を考えると、py < ymin[px] またはpy > ymax[px] ならその点は可視であり、ymin[px] またはymax[px]を更新する。4)
曲面関数生成ここでの関数FuncHatの例は z = z0(cos(sgrt(x2+y2)) + cos(3sqrt(x2+y2))) を用いた。戻り値は3次元座標を表現できるPoint3クラスである。
public Point3 FuncHat(int x, int y) { // Function z = F(x,y) double rd= Math.PI/180; double r = Math.sqrt(x*x+y*y)*rd; double z = 30.*(Math.cos(r) + Math.cos(3.*r)); return new Point3(x, y, z); } |
ワイヤーフレーム表示とは、隠線消去を行なわないで表示する方法である。ここでは簡単のため凸多面体の代表的な立方体を対象とする。 視点は極座標(R、θ、Φ)で与える。マウスをドラッグして動かすと、視点が変化できるようにする。この際、マウスを左右するとθ、上下させるとΦが変化するものとする。 基本的には、次の手順からなる。
立方体の場合、8頂点と6面からなる。これを記憶するため、頂点の
3次元座標を記憶するクラス Point3 (5.3参照)と、面を記憶するクラス Plane を準備する。また、座標変換のためのクラスTrans (5.3参照)も利用する。多面体は頂点および面に番号を付けし、各頂点の3次元座標はpoint[ ] (クラス Point3)に、面はその構成頂点を face[ ] (クラス Plane)に記憶する(データ構造は資料[1]参照)。描画は面単位に行なう(クラス Plane のメソッドdrawを利用)。この方法では、辺は2つの面の稜線であるため辺は2度描きされている。
ここで使用している配列とクラスとの関係は下記のようである。
オブジェクト |
配列名 |
クラス |
内容 |
面 |
face[ ] |
Plane |
i1,i2,i3,i4 ;頂点番号 |
3 次元頂点 |
point[ ] |
Point3 |
x,y,z ;3次元実数 |
2 次元頂点(投影後) |
tPoint[ ] |
Point |
x, y ;2次元整数 |
1)
立方体の描画関数
Plane face[ ]; // 面オブジェクトの生成 Point3 point[ ]; // 頂点オブジェクトの生成 Trans henkan = new Trans() ; // 座標変換オブジェクトの生成 Public void paint(Graphics g) { Point [ ] tPoint = new Point [npoints]; // 透視変換後の頂点座標配列
Henkan.preper(); // 座標変換の係数セットFor (int i = 0; i < npoints; I++) // 各頂点の座標変換 tPoint[i] = henkan.project( point[i] ); for(int i=0;i<nplanes; i++) // 各面の描画face[i].draw(g, x_origin, y_origin, tPoint); } |
2)
立方体生成の関数;
public void model(double scale) { // 立方体生成double pnt[ ][ ]={ // 頂点座標{1.000000, -1.000000, 1.000000}, {1.000000, 1.000000, 1.000000}, {1.000000, 1.000000, -1.000000}, {1.000000, -1.000000, -1.000000}, {-1.000000, 1.000000, 1.000000}, {-1.000000, -1.000000, 1.000000}, {-1.000000, -1.000000, -1.000000}, {-1.000000, 1.000000, -1.000000} }; int element[ ][ ] = { // 面の構成頂点{ 0, 1, 2, 3},{1, 4, 7, 2}, {4, 5, 6, 7}, {0, 3, 6, 5}, {5, 4, 1, 0}, {2, 7, 6, 3} }; npoints = 8; point = new Point3[npoints]; // 頂点の配列確保for(int k=0; k<npoints; k++) // 頂点座標の記憶 (scale; 立方体のサイズ) point[k] =new Point3(scale*pnt[k][0],scale*pnt[k][1],scale *pnt[k][2]); nplanes = 6; face = new Plane[nplanes]; // オブジェクトの生成 (面の配列確保)for(int i=0; i<nplanes; i++){ face[i] = new Plane(); for(int j=0; j<4; j++) face[i].add(element[I][j]);} // 各面の頂点追加} |
面(多角形)の構成番号を記憶し、描画する。
class Plane {public int set[ ]; // 面の構成頂点の配列 public int npoint=0; // 面の頂点数 Plane() { set = new int[4]; } // 面の構成頂点番号の配列の確保public void add(int k) { // 頂点番号の追加set[npoint] = k; npoint ++; } public void draw(Graphics g, int x_origin,int y_origin,Point point[]) { for (int i = 0; i < npoint; i++) { int ii = i+1; if(ii==npoint) ii=0; g.drawLine(point[set[I]].x+x_origin, y_origin-point[set[i]].y, // 各辺の描画point[set[ii]].x+x_origin, y_origin ? point[set[ii]].y); } } } |
6.
その他1)
下記ディレクトリからJavaソースおよびHTMLファイルをコピーして練習して下さい。ecc ~knis1482/sougou/source/
Java ソース |
HTML |
|
Bresenham の直線描画 |
BresenS.java 、Bresen.java |
BresenS.html, Bresen.html |
コッホ曲線の描画 |
Koch.java |
coch.html |
ガスケットの表示 |
gasket.java |
gasket.html |
多面体のワイヤーフレーム表示 |
wire.java |
wire.html |
BresenS.java
はBresen.javaのマウス部分が無いものである。JDK.1.1とJDK.1.0に対応するため、JDK.1.1用のものはJDK.1.0部分はコメントとしている。JDK.1.0対応のものはコンパイル時に警告が出るが、実行可能なので警告は無視して下さい。練習問題を行なうに当たっては、マウスの機能の有無、背景や線の色の変更、画面サイズの変更、見る方向変更などを試みて、自分なりのプログラムに修正して下さい。
乱数を使用して2端点の座標を生成し、複数の線分をスクリーン中に描画する。なお、生成した端点の座標がスクリーン内になるように制限すること。
また、練習課題に関する最新情報は
~knisi1482/index.html をブラウザーで起動して参考にして下さい。2)
本科目に関する情報(課題等)は、下記URLを参照して下さい。/~nis/junk/sougou.html
http://java.sun.com/products/jdk/1.2/ja/
参考資料
プログラム実習
(総合科目)<
練習問題を行なうに当たっては、マウスの機能の有無、背景や線の色の変更、画面サイズの変更、見る方向の変更などを試みて、下記のように自分なりのプログラムに修正して下さい。
・HTMLファイルで描画する2端点の座標を指定できるように変更してみる。
・乱数を使用して2端点の座標を生成し、複数の線分をスクリーン中に描画する。なお、生成した端点の座標がスクリーン内になるように制限すること。