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
}
}