JavaFXで3D地形シミュレーション
JavaFXで地形をシミュレーションをしてみたので紹介します。
シュミレーションと言っても名ばかりで、「大体似たような感じ」という程度です。
地球全体に雨を降らせ、地面の高いところから低いところへ水を流します。水が流れる時に土を少しだけ一緒に運ぶようにしました。
計算を続けると雨でだんだん地面が削られて谷ができていきます。
仕組み
二次元の計算スペースを同じ大きさの正方形ピクセルで埋め尽くします。各ピクセルには水の量と土の量を入れておきます。
最初に雨を降らせて全てのセルに水を足します。
各ピクセルで隣の八つのピクセルとの高さの差(土の量+水の量)を調べます。水を高いところから低いところへ移動させます。移動する水の量は、高さの差があるほど多くします。
水の移動と同時に土も移動させます。いわゆる泥水ですね。
土の量は水の移動量に応じて増やします。ただし、移動先の土の量が移動元よりも多い場合は移動させません。
JAPANFXで3D化する方法JavaFX楽天 で地形のような形状を表現するときはMeshViewを使います。メッシュビューに3D形状を与えるには、TriangleMeshを使います。
雨が降って変わった高さをまずTriangleMeshオブジェクトに入れ、このTriangleMeshオブジェクトをMeshViewオブジェクトに入れると高さが変わります。
計算時間
動画はかなり早送りで動かしています。性能の良いパソコンでもこの10倍ぐらいの時間がかかります。動きを早くするには水の量を増やしたり移動量を増やしたりすれば良いのですが、増やしすぎると計算がおかしくなってフリーズしてしまいます。
なので少しずつしか数値を変えられず、計算時間が長くなってしまいます。
処理の流れ
まずステージを作り、ボタンなどをレイアウトします。
3D表示のためのメッシュビューを作って、計算のためのピクセルを作ります。
Pixelに初期の土の量を入れ、これをメッシュビューに反映させます。作例では三角関数で高さを計算し、これに乱数を加えています。
これで準備完了。
開始ボタン楽天 を押すとタイマーが有効になり、無限ループの繰り返し計算が始まります。
くりかえし計算の最初で雨を降らせます。雨はすべてのピクセルに一定の量の水を加えています。
水はほんの少しづつ加えます、多すぎるとフリーズしてしまいます。
次に水と土の移動計算を3000回繰り返します。たくさん繰り返さないと水が溜まってできた湖の水面が水平にならないです。
最後に変化した水と土の量をピクセルからMeshViewオブジェクトに与えて3D表示を変更します。
結果
平野を流れるくねくねした川が作りたいなと思っていたのですが、今のところできないでいます。
水と土の移動設定が良くないのか、ピクセル数が少ないのか、計算時間が少ないのか原因はよくわかりません。現状では砂山に雨が降って砂が崩れたような形状になります。
砂場に雨が降ったような感じですね。
このあたりいろいろ変えてみてどうなるか見てみようと思っています。
ソースコード
主要な部分のソースコードは下記です。実際に動かすには、他にFlatMeshクラス、ThreeDStageSが必要になります。
事情により他のClassは公開しません。
package tomojavalib.kawa;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point3D;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.layout.FlowPane;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
import tomojavalib.threedstage.ThreeDStageS;
import tomojavalib.mesh.FlatMesh;
public class KawaStage extends Application {
//ステージ
public ThreeDStageS stage = null;
TriangleMesh mesh = null;
FlatMesh fm = null;
MeshView meshView = null;
Pixel[][] pixel =null;
Pixel[] pxs = null;
Button[] button = null;
//タイマー
public Timeline timer=null;
//タイマーを止めるフラグ
boolean timerstopflug = false;
//タイマー実行中を示すフラグ
public boolean timerruningflug = false;
//表示フラグ
boolean wflug = true;
//---メイン---
public static void main(String[] args) { Application.launch(args); }
@Override
public void start( Stage stage ) throws Exception {
this.stage = new ThreeDStageS();
//カメラ位置調整
this.stage.cameratranslateY.setY( 0 );
//this.stage.cameratranslate.setX( 0 );
//this.stage.cameratranslate.setY( 0 );
//this.stage.cameratranslate.setZ( 0 );
//this.stage.camerarotateZ.setAngle( 30 );
//this.stage.camerarotateX.setAngle( -60 );
//this.stage.camerarotateY.setAngle( 0 );
//ボタンなどレイアウト
this.layoutComponents( this.stage.pane );
//ピクセル数設定
int px =100; int py=100;
//mesh作成
this.makeMesh(px,py);
//Pixsel作成
this.makePixel(px,py);
//ピクセルに初期土高さを入れる
this.setTutiHeight();
//メッシュに高さを入れる
this.setHeigth();
//タイマースタート
//this.timerStart();
return;
}
/**ボタンなどをレイアウトする*/
private void layoutComponents( FlowPane pane ) {
button = new Button[2];
button[0] = new Button("開始");
button[1] = new Button("表示");
pane.getChildren().addAll( button );
button[0].setOnMouseClicked((MouseEvent)->{ if(this.timerruningflug) {this.timerStop();}else{this.timerStart();} });
button[1].setOnMouseClicked((MouseEvent)->{
if(this.wflug) { this.wflug= false; }else{ this.wflug= true; } });
return;
}
/**ピクセルに初期高さを入れる*/
private void setTutiHeight() {
double d = -50.;
for(int i=0;i<fm.p.length;i++) {
double l = Math.sqrt((d-fm.p[i].d.x )*(d-fm.p[i].d.x )+(d-fm.p[i].d.y )*(d-fm.p[i].d.y ));
double z = Math.cos( l/50 )*20 + Math.random()*1+100;
pxs[i].t = z;
}
}
/**pixel作成*/
private void makePixel(int px, int py ) {
//ピクセル作成
pixel = new Pixel[px+2][py+2];
for(int ix=0;ix<pixel.length;ix++) {
for(int iy=0;iy<pixel[ix].length;iy++) {
pixel[ix][iy] = new Pixel();
}}
//pxs作成
pxs = new Pixel[ px*py ];
int i=0;
for(int ix=1;ix<pixel.length-1;ix++) {
for(int iy=1;iy<pixel[ix].length-1;iy++) {
pxs[i] = pixel[ix][iy] ;
i++;
}}
i=0;
for(int ix=1;ix<pixel.length-1;ix++) {
for(int iy=1;iy<pixel[ix].length-1;iy++) {
pxs[i].np = new Pixel[8];
pxs[i].np[0] = pixel[ix-1][iy];
pxs[i].np[1] = pixel[ix+1][iy];
pxs[i].np[2] = pixel[ix][iy+1];
pxs[i].np[3] = pixel[ix][iy-1];
pxs[i].np[4] = pixel[ix-1][iy-1];
pxs[i].np[5] = pixel[ix+1][iy+1];
pxs[i].np[6] = pixel[ix-1][iy+1];
pxs[i].np[7] = pixel[ix+1][iy-1];
i++;
}}
return;
}
private void makeMesh(int x, int y ) {
//Group group = new Group();
//メッシュを置く
fm = new FlatMesh();
fm.makeNewSheet( 1000./((double)x), 1000./((double)y) , -500, -500, x , y , 1 , 0 , 0 , 1 , 1 );
meshView = new MeshView();
mesh = fm.cleateTriangleMesh();
meshView.setMesh( mesh );
meshView.setDrawMode( DrawMode.FILL);
PhongMaterial material = new PhongMaterial();
Image img = new Image("file:d:/work/java/test.png");
material.setDiffuseMap( img );
meshView.setMaterial(material);
meshView.setCullFace(CullFace.NONE);
this.stage.group.getChildren().add( meshView );
meshView.getTransforms().add( new Rotate( 90. , new Point3D( 1 , 0 , 0 ) ) );
}
private void setHeigth() {
for(int i=0;i<fm.p.length;i++) {
// fm.p[i].d.z = z;
fm.p[i].d.z = -pxs[i].t;
if( wflug ) { fm.p[i].d.z = -pxs[i].getHeigth(); }else { fm.p[i].d.z = -pxs[i].t;}
// fm.p[i].d.z = -pxs[i].w;
}
TriangleMesh mesh = fm.cleateTriangleMesh();
meshView.setMesh( mesh );
return;
}
/**地形を動かす*/
private void move() {
//System.out.println( "move" );
//周囲の土地高さを整える
this.setSyuui();
//dt twを0にセット
this.resetDtDw();
//水と土の移動量をセット
this.moveDosya();
//水と土を移動させる
this.setDosya();
}
/**土砂を移動させたものを組み込む*/
private void setDosya() {
for(int i=0;i<pxs.length;i++) {
pxs[i].t = pxs[i].t + pxs[i].dt;
pxs[i].w = pxs[i].w + pxs[i].dw;
}
return;
}
/**土砂を移動させる*/
private void moveDosya() {
//隣とのt+wとの差がある場合、
//差の1/100の水を移動させる。
//移動した水の量の1/1000の土を移動させる
for(int i=0;i<pxs.length;i++) {
this.moveNext( pxs[i] , pxs[i].np[0] );
this.moveNext( pxs[i] , pxs[i].np[1] );
this.moveNext( pxs[i] , pxs[i].np[2] );
this.moveNext( pxs[i] , pxs[i].np[3] );
this.moveNext( pxs[i] , pxs[i].np[4] );
this.moveNext( pxs[i] , pxs[i].np[5] );
this.moveNext( pxs[i] , pxs[i].np[6] );
this.moveNext( pxs[i] , pxs[i].np[7] );
}
return;
}
/**隣のピクセルへ移動する*/
private void moveNext( Pixel from ,Pixel to ) {
double sa = from.getHeigth() - to.getHeigth();
double dt = 0;
double dw = 0;
double d =40;
//if( sa>10000 ) { System.out.println( from.w ); }
if( sa>0 ) {
if(from.w>0) {
if( sa<from.w ) {dw = sa /d ;}else{dw = from.w /d ;}
}else { dw=0; }
}else {
if(to.w>0) {
sa = -sa;
if( sa<to.w ) {dw = sa /d ;}else{dw = to.w /d ;}
dw =-dw;
}else { dw=0; }
}
from.dw = from.dw-dw;
to.dw= to.dw+dw;
// System.out.println( to.w );
double ddt = from.t -to.t;
dt = dw/(to.w+1)*ddt/100. ;
if(ddt>0) {if(dw>0) {
from.dt = from.dt-dt;
to.dt = to.dt+dt;
}}
if(ddt<0) {if(dw<0) {
from.dt = from.dt+dt;
to.dt = to.dt-dt;
}}
return;
}
/**水と土の移動量をリセット*/
private void resetDtDw() {
for(int i=0;i<pixel.length;i++) {
for(int ii=0;ii<pixel[i].length;ii++) {
pixel[i][ii].dt =0;
pixel[i][ii].dw =0;
}}
return;
}
/**周囲の高さ一つ内側と同一にする*/
private void setSyuui() {
for(int i=0;i<pixel.length;i++) {
pixel[i][0].t = pixel[i][1].t;
pixel[i][ pixel[i].length-1 ].t = pixel[i][ pixel[i].length-2 ].t;
pixel[i][0].w = pixel[i][1].w;
pixel[i][ pixel[i].length-1 ].w = pixel[i][ pixel[i].length-2 ].w;
}
for(int i=0;i<pixel[0].length;i++) {
pixel[0][i].t = pixel[1][i].t;
pixel[0][i].w = pixel[1][i].w;
}
for(int i=0;i<pixel[ pixel.length-1 ].length;i++) {
pixel[ pixel.length-1 ][i].t = pixel[pixel.length-2][ i ].t;
pixel[ pixel.length-1 ][i].w = pixel[pixel.length-2][ i ].w;
}
for(int i=40;i<60;i++) {
pixel[ 0 ][i].t = 0; pixel[ 0 ][i].w = 0;
}
}
/**雨を降らせる*/
private void rain() {
for(int i=0;i<pxs.length;i++) {
//for(int i=7000;i<pxs.length;i++) {
pxs[i].w = pxs[i].w +0.1;
}
}
/**
* */
public void timerStart() {
//timerがすでに走っている場合は何もしない
if( timerruningflug ==true ) { return; }
//タイマー実施フラグをセットする
timerruningflug =true;
//timerがすでにできている場合は再開
if( timer!=null ) { timer.play(); return; }
//タイマーをセットする
timer = new Timeline(new KeyFrame(Duration.millis(1), new EventHandler<ActionEvent>(){
int keisan=0;
//繰り返し作業の中身
@Override
public void handle( ActionEvent event ){
keisan++;
//一旦タイマーとめる
timer.pause();
//フラグが立っていたら、何もせずに帰る
if( timerstopflug ) {
timerstopflug = false;
timerruningflug = false;
return;
}
//雨を降らせる
rain();
//水と土の移動
for(int i=0;i<3000;i++) {
move();
}
//地形の変形
setHeigth();
System.out.println( keisan +" "+ pxs[250].getHeigth() +" "+ pxs[250].dt +" "+ pxs[250].dw );
//規定の計算回数になったら止める
if( keisan>200 ) { keisan=0; }
//タイマー再開
timer.play();
return;
}
}));
timer.setCycleCount(Timeline.INDEFINITE);
timer.play();
return;
}
/**タイマーの一時停止*/
public void timerStop() {
//this.timer.pause();
timerstopflug = true;
return;
}
}
最終更新日: 2020-07-06 10:26:44