総合科目
Java
文法および例題の説明
情報科学科 西田 友是
(1)Bresenhamの直線描画 (Bresen.Java)
(2)コッホ曲線の描画 (Koch.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)
実数演算をさけるため、誤差の式から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.2 コッホの曲線(レベル5の場合)コッホ曲線の生成過程の基本アルゴリズムは次のようである。
プログラムの説明
(
a)x0、y0は、線分の開始位置を示し、angleは、線分の角度を表す。(
b) 1本の線分の長さ(leng)を計算する。(
c) コッホ曲線を1本作成するメソッド(draw)を呼び出す。(
d) n回のコッホ曲線を作成するメソッドを呼び出す。線分の角度を0度、60度、―120度、60度回転させて直線を引く。(
e) 次の点のx、y座標は次式で計算する。x= leng・cos(60°)
y = leng
・sin(60°)(f) 0
度、60度、-120度、60度回転させてdrawを再帰的に呼び出す。
ここでの例は、クラス
KochCurvを使う方法とした。このクラスに描き初めの座標(x0,y0)、角度(angle)、長さ(leng)、および繰り返しレベル nを指定するすることによる再帰的描画方法とする。
1)
コッホ曲線のクラス;曲線の初期値設定および再帰的描画
class KochCurv {double x0,y0, angle; // (x0,y0);端点座標、angle;角度 KochCurv() { } // コンストラクター
public void draw(int I, double leng, Graphics gr) { If ( i == 0) { double rd = 3.1415927/180.; double x = leng * Math.cos(rd * angle) + x0 ; double y = -leng * Math.sin(rd * angle) + y0; gr.drawLine((int)x0,(int)y0,(int)x, (int)y); // 線分を描画x0 = x; y0 = y; } // 終点を次の線分の始点に置き換え else { draw(i ? 1, leng, gr); angle += 60.; draw(i ? 1, leng, gr); angle -= 120.; draw(i ? 1, leng, gr); angle += 60.;; draw(i ? 1, leng, gr); } }
public void setPoint(double lpx, double lpy) { // 始点を設定x0= lpx ; y0 = lpy ; } public void setangle(double a) { // 角度の初期値設定angle = a; } } |
2)
コッホ曲線の描画関数:
public void paint(Graphics g) { KochCurv koch = new KochCurv(); // オブジェクトの生成koch.setPoint(30.0, 140.); // 始点の座標指定 koch.setangle(0.0); //角度の初期値設定 double leng = 400 * Math.pow(0.33333, n); koch.draw(n, leng, g); // コッホ曲線描画} |
このプラグラムは再帰呼び出しの回数を
HTMLによって指定できるが、下記のようにvalueの値でしていする。なお、デフォルトは5回である。
<param name="n" value="1">
この隠線消去はスクリーン空間で行われる。基本的な考え方は、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)
曲線を描画する関数
}
ここで、(
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 |
一価関数曲面の表示 |
hidfunc.java |
hidfunc.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/
参考資料