import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; /* */ /** * 球面上の最も離れた点はどこにあるか? */ public class Hello3D_Sphere6 extends Applet implements MouseMotionListener, MouseListener, Runnable, ActionListener{ private static final long serialVersionUID = -8931409060482821778L; ArrayList earth; // ベースとなる球面 ArrayList komas; // 球面上に駒を置いてみる int N_KOMA = 7; // 駒の数 double CO_ANTI = 0.02; // 反発係数 Point center; // アプレットの中心座標 Point mousePosition; // マウス位置 double scale; // モデル描画時のスケール double phi; // x軸周りの回転角 double theta; // y軸周りの回転角 Image bufferImage; // ダブルバッファリング用のイメージ Dimension appletSize; // アプレットサイズ static final int BUTTON_MARGIN = 40; // コンポーネント分の高さ // コンポーネント Canvas theCanvas; TextField txt_koma; // 駒の数 TextField txt_anti; // 反発係数 Button btn_run; Thread main_th; // 実行スレッド public void init() { // アプレットサイズの取得 appletSize = getSize(); // アプレット全体のレイアウト this.setLayout(new FlowLayout()); // 描画領域の作成 theCanvas = new Canvas(); theCanvas.setBackground(Color.BLACK); theCanvas.setSize( appletSize.width, appletSize.height - BUTTON_MARGIN ); theCanvas.addMouseMotionListener(this); theCanvas.addMouseListener(this); this.add(theCanvas); // テキスト入力 -- 駒の数 this.add( new Label("N-Points:") ); txt_koma = new TextField(); txt_koma.setText( String.valueOf(N_KOMA) ); this.add(txt_koma); // テキスト入力 -- 反発係数 this.add( new Label("Co-Effect:") ); txt_anti = new TextField(); txt_anti.setText( String.valueOf(CO_ANTI) ); this.add(txt_anti); // ボタン btn_run = new Button("Run"); btn_run.addActionListener( this ); this.add( btn_run ); // アプレットの中心座標の取得 center = new Point(appletSize.width / 2, (appletSize.height - BUTTON_MARGIN) / 2); // 描画スケールの設定 scale = appletSize.width * 0.8 / 2; // ダブルバッファリング用のイメージを作成 if(bufferImage == null) { bufferImage = createImage(appletSize.width, (appletSize.height - BUTTON_MARGIN)); } // アプリケーションの初期化 initApp(); //自分を渡してThreadクラスを作成しスレッド起動 main_th = new Thread(this); main_th.start(); } // アプリケーションの初期化 private void initApp(){ // マウス位置の初期化 mousePosition = new Point(0, 0); // 回転角の初期化 phi = 0.0; theta = 0.0; // モデルデータの設定 setModelData(); // 頂点のスクリーン座標の設定 setScreenPosition(); } public void paint(Graphics g) { // バッファにモデルを描画 drawModel(bufferImage.getGraphics()); // バッファイメージをアプレットに描画 Graphics gc = theCanvas.getGraphics(); gc.drawImage(bufferImage, 0, 0, this); } // 描画更新時に背景の塗りつぶし処理を行わないためのオーバーライド public void update(Graphics g) { paint(g); } // ボタンが押されたときの処理 public void actionPerformed( ActionEvent ev ) { boolean is_error = false; // 実行ボタン -- アプリの再実行 if( ev.getSource() == btn_run ) { // txt_koma の入力値を得る try{ N_KOMA = Integer.parseInt( txt_koma.getText() ); } catch(NumberFormatException ex ){ // ex.printStackTrace(); txt_koma.setText( String.valueOf(N_KOMA) ); is_error = true; } // txt_antiの入力値を得る try{ CO_ANTI = Double.parseDouble( txt_anti.getText() ); } catch(NumberFormatException ex ){ // ex.printStackTrace(); txt_anti.setText( String.valueOf(CO_ANTI) ); is_error = true; } if( ! is_error ){ // エラーが無ければ initApp(); // アプリケーションの再初期化 } } } // マウスイベント public void mouseMoved(MouseEvent e) {} public void mouseClicked(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseDragged(MouseEvent e) { // 回転角の更新 theta += (e.getX() - mousePosition.x) * 0.01; phi += (e.getY() - mousePosition.y) * 0.01; // x軸周りの回転角に上限を設定 phi = Math.min(phi, Math.PI/2); phi = Math.max(phi, -Math.PI/2); // マウス位置の更新 mousePosition.setLocation(e.getX(), e.getY()); // 頂点のスクリーン座標の更新 setScreenPosition(); // 描画更新 repaint(); } public void mousePressed(MouseEvent e) { // マウス位置の更新 mousePosition.setLocation(e.getX(), e.getY()); } // モデルデータの設定 private void setModelData() { earth = new ArrayList(); komas = new ArrayList(); // ベース球面モデルの作成 earth.add( new VertexR( 0, Math.PI ) ); // 北極 for(double z = -1.0+0.25; z < 1.0; z += 0.25 ){ for(int i=0; i < 12; i++ ){ earth.add( new VertexR( i * 2 * Math.PI / 12 , Math.acos( z ) ) ); } } earth.add( new VertexR( 0, 0 ) ); // 南極 // 駒の初期化 for(int i=0; i < N_KOMA; i++ ){ VertexK koma = sphereRndKoma(); komas.add( koma ); } } // 駒をランダムな位置に作成する private VertexK sphereRndKoma() { double z = Math.random() * 2 -1; double rs = Math.random() * Math.PI * 2; return new VertexK( rs, Math.acos(z) ); } // 頂点のスクリーン座標を更新する private void setScreenPosition() { for(int i = 0; i < earth.size(); i++) { VertexR v = (VertexR)earth.get(i); v.rotate(theta, phi); // 回転後の座標値の算出 v.project(center, scale); // スクリーン座標の算出 } for(int i = 0; i < komas.size(); i++) { VertexK koma = (VertexK)komas.get(i); koma.rotate(theta, phi); koma.project(center, scale); } } // モデルの描画 private void drawModel(Graphics g) { final int KOMA_SIZE = 4; // 全体をクリア g.setColor(Color.BLACK); g.fillRect(0, 0, appletSize.width, appletSize.height); // ベース球面の描画 g.setColor(Color.GREEN); drawEarth(g); // 駒の描画 for(int i = 0; i < komas.size(); i++) { VertexK koma = (VertexK)komas.get(i); if( koma.ry >= 0.0 ){ // 手前と奥で色を変える g.setColor(Color.YELLOW); }else{ g.setColor(Color.BLUE); } g.fillOval(koma.screenX - KOMA_SIZE, koma.screenY - KOMA_SIZE, 2 * KOMA_SIZE, 2 * KOMA_SIZE); } } // ベース球面の描画 -- 頂点をうまい具合に線で結ぶ private void drawEarth(Graphics g) { VertexR v1, v2; int i; int base = 1; // 経線(横線)を描く // for( double z = -1.0+0.25; z < 1.0; z += 0.25 ){ for( int j= -3; j < 4; j++ ){ for(i=0; i < 12 -1; i++ ){ v1 = (VertexR)earth.get(base + i); v2 = (VertexR)earth.get(base + i + 1); // 1つ先の頂点と結ぶ g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY ); } // 円の最後を閉じる v1 = (VertexR)earth.get(base + i); v2 = (VertexR)earth.get(base); g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY ); base += (++i); } // 緯線(縦線)の北極回り base = 0; v1 = (VertexR)earth.get(base); base = 1; for(i=0; i < 12; i++ ){ v2 = (VertexR)earth.get(base + i); g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY ); } // 緯線を描く base = 1; // for( double z = -1.0+0.25; z < 1.0-0.25; z += 0.25 ){ for( int j= -3; j < 3; j++ ){ for(i=0; i < 12; i++ ){ v1 = (VertexR)earth.get(base + i); v2 = (VertexR)earth.get(base + i + 12); // 12個先の頂点と結ぶ g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY ); } base += i; } // 緯線(縦線)の南極回り v2 = (VertexR)earth.get( earth.size() - 1 ); for(i=0; i < 12; i++ ){ v1 = (VertexR)earth.get(base + i); g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY ); } } public void destroy() { main_th = null; //スレッドの終了 } public void run() { while(main_th != null){ // 駒を動かす moveKomas(); // 頂点のスクリーン座標の更新 setScreenPosition(); repaint(); // 再描画 try // 時間待ち { Thread.sleep(50); } catch( InterruptedException ex ) { ex.printStackTrace(); } } } // 駒を動かす private void moveKomas(){ // 自分以外の全ての相手との相互作用を得る for(int i = 0; i < komas.size(); i++) { VertexK koma1 = (VertexK)komas.get(i); // koma1を北極にもっていく角度 double rot_th = koma1.th - Math.PI/2; double rot_ph = - koma1.ph; double sum_x = 0.0, sum_y = 0.0; for(int j = 0; j < komas.size(); j++) { if( i == j ){ // 自分自身との相互作用は無い continue; } VertexK koma2 = (VertexK)komas.get(j); double save_x = koma2.x; // 現状を保存 double save_y = koma2.y; double save_z = koma2.z; double save_th = koma2.th; double save_ph = koma2.ph; // koma1を北極にもっていったとき、koma2はこの位置にくる koma2.rotate( rot_th, rot_ph ); // x, y, z -> rx, ry, rz koma2.x = koma2.rx; koma2.y = koma2.ry; koma2.z = koma2.rz; koma2.DtoR(); // koma2への変異を足し合わせる double r; if( koma2.ph > 0.00001 ){ // 0割りを防ぐ r = 1 / koma2.ph; // 角度の逆数を影響力と見なす }else{ r = 1 / 0.00001; } sum_x += r * Math.cos( koma2.th ); sum_y += r * Math.sin( koma2.th ); koma2.x = save_x; // 現状を復帰 koma2.y = save_y; koma2.z = save_z; koma2.th = save_th; koma2.ph = save_ph; } koma1.dx = - sum_x; koma1.dy = - sum_y; // 斥力だからマイナス } // 相互作用に従って koma1 を移動する for(int i = 0; i < komas.size(); i++) { VertexK koma1 = (VertexK)komas.get(i); // koma1を北極にもっていく角度 double rot_th = koma1.th - Math.PI/2; double rot_ph = - koma1.ph; double sum_x = koma1.dx; double sum_y = koma1.dy; // koma1.rotate( rot_th, rot_ph ); // 回転して北極に持ってくる // x, y, z -> rx, ry, rz // koma1.x = 0.0; // koma1.rx; // koma1.y = 0.0; // koma1.ry; // koma1.z = 1.0; // koma1.rz; // 実際に北極に持ってゆく計算は不要、どうせ上書きするので。 double r2 = sum_x * sum_x + sum_y * sum_y; koma1.th = VertexK.atanXY( sum_x, sum_y ); koma1.ph = CO_ANTI * r2; koma1.RtoD(); // th, ph -> x, y, z koma1.rotate2( rot_th, rot_ph ); // 北極で微小移動した駒を元に戻す // x, y, z -> rx, ry, rz koma1.x = koma1.rx; koma1.y = koma1.ry; koma1.z = koma1.rz; koma1.DtoR(); // x, y, z -> th, ph } } } /** * 運動する頂点クラス */ class VertexK extends VertexR { public double dx, dy; // 位置の変異 = 運動 public VertexK(double th, double ph) { super(th, ph); } public VertexK(double x,double y,double z) { super(x,y,z); } } /** * 頂点クラス */ class VertexR { public double th, ph; // モデルの頂点角度座標 public double x, y, z; // モデルの頂点座標 public double rx, ry, rz; // 回転させた後の座標 public int screenX, screenY; // スクリーン上の座標 // コンストラクター、角度から public VertexR(double th, double ph) { this.th = th; this.ph = ph; RtoD(); } // 角度 -> XYZ public void RtoD(){ this.x = Math.cos(this.th) * Math.sin(this.ph); this.y = Math.sin(this.th) * Math.sin(this.ph); this.z = Math.cos(this.ph); } // コンストラクター、XYZから public VertexR(double x,double y,double z) { this.x = x; this.y = y; this.z = z; } // 回転する public void rotate(double theta, double phi) { // 回転後の座標値の算出 this.rx = + this.x * Math.cos(theta) + this.y * Math.sin(theta); this.ry = - this.x * Math.cos(phi) * Math.sin(theta) + this.y * Math.cos(phi) * Math.cos(theta) + this.z * Math.sin(phi); this.rz = + this.x * Math.sin(phi) * Math.sin(theta) - this.y * Math.sin(phi) * Math.cos(theta) + this.z * Math.cos(phi); } /* -- 回転変換 X軸回りに-φ回転 ○ Z軸回りに-θ回転 = 合成 1 0 0 cosθ sinθ 0 cosθ sinθ 0 0 cosφ sinφ -sinθ cosθ 0 -cosφsinθ cosφcosθ sinθ 0 -sinφ cosφ 0 0 1 sinφsinθ -sinφcosθ cosφ */ /* -- 逆変換 Z軸回りにθ回転 ○ X軸回りにφ回転 = 合成 cosθ -sinθ 0 1 0 0 cosθ -sinθcosφ sinθsinφ sinθ cosθ 0 0 cosφ -sinφ sinθ cosθcosφ -cosθsinφ 0 0 1 0 sinφ cosφ 0 sinφ cosφ */ // 逆回転する public void rotate2(double theta, double phi) { // 回転後の座標値の算出 this.rx = + this.x * Math.cos(theta) - this.y * Math.sin(theta) * Math.cos(phi) + this.z * Math.sin(theta) * Math.sin(phi); this.ry = + this.x * Math.sin(theta) + this.y * Math.cos(theta) * Math.cos(phi) - this.z * Math.cos(theta) * Math.sin(phi); this.rz = + this.y * Math.sin(phi) + this.z * Math.cos(phi); } // XYZ->角度 public void DtoR(){ this.ph = Math.acos(this.z); this.th = atanXY(this.x, this.y); } // X,Y 座標から角度を逆算する public static double atanXY(double x0, double y0) { double retval; if( x0 == 0 && y0 == 0 ){ // 0割を防ぐ return 0.0; } if( y0 <= x0 ){ if( y0 > - Math.abs(x0) ){ retval = Math.atan( y0 / x0 ); } else{ retval = Math.PI / 2 - Math.atan( x0 / y0 ) + Math.PI; } } else{ // this.y > this.x if( y0 > Math.abs(x0) ){ retval = Math.PI / 2 - Math.atan( x0 / y0 ); } else{ retval = Math.atan( y0 / x0 ) + Math.PI; } } return retval; } // スクリーン上に投影する // X軸が横、Z軸が縦、Y軸を奥行きとしている // 通常の、Z軸が奥行きという座標系とは異なっているので注意 public void project(Point center, double scale) { this.screenX = (int)(center.x + scale * this.rx ); this.screenY = (int)(center.y - scale * this.rz ); // <- ここが rz } }