今回は、3Dゲームでよく使われるビルボードをJavaFXで実装する方法について見ていく。
■ ビルボードとは
ビルボードを簡単に言うと、常にカメラ方向に向いている面である。ビルボードを利用することで3Dの世界に2Dの画像を埋め込むことができるため、3Dゲームのエフェクトや文字表示などに利用される。その他、背景には3Dを利用するが、キャラクタだけは2D画像を利用したいという場合にもビルボードが利用される。
また、ビルボードを利用することでポリゴン数が削減できるため、3Dオブジェクトを2次元画像で代用して処理を軽量化する用途にも利用される。
■ サンプルプログラム
以下にJavaFXでビルボードを実装するサンプルプログラムを示す。サンプルでは、xyz軸とキャラクター画像が描かれたビルボードを1つ表示している。画面をクリックするとカメラがx軸を中心とした回転を始める。しかし、ビルボードは常にカメラを向いているため、キャラクター画像が動いていないように見えることが確認できる。。
◇サンプルコード
package application_fx_functional;
import java.io.File;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Mesh;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Affine;
import javafx.scene.transform.NonInvertibleTransformException;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TestBillBoard extends Application {
public static void main(String[] args)
{
launch( args );
}
@Override
public void start(Stage primaryStage) throws Exception
{
// シーングラフの構成
Group root = new Group();
// シーンの作成
// 3Dシーンの奥行きを表現するため、Zバッファを有効にする
Scene scene = new Scene( root , 600 , 480 , true );
scene.setFill( Color.GRAY );
// カメラ設定
PerspectiveCamera camera = new PerspectiveCamera( true );
camera.setFarClip( 3000 );
camera.getTransforms().add( new Rotate( 60 , -camera.getTranslateX() , -camera.getTranslateY() , -camera.getTranslateZ() , Rotate.X_AXIS ) );
camera.getTransforms().add( new Rotate( 30 , -camera.getTranslateX() , -camera.getTranslateY() , -camera.getTranslateZ() , Rotate.Y_AXIS ) );
camera.getTransforms().add( new Translate( 0 , 0 , -10 ) );
scene.setCamera( camera );
// 中心軸を表示
// x軸=赤、y軸=青、z軸=緑
Color[] axisColor = { Color.RED , Color.BLUE , Color.GREEN };
double[] axisSize = { 10.0 , 0.05 , 0.05 ,
0.05 , 10.0 , 0.05 ,
0.05 , 0.05 , 10.0 };
for( int i = 0 ; i < 3 ; i++ )
{
// 軸船を作成
Box axis = new Box();
axis.setWidth( axisSize[ i*3 ] );
axis.setHeight( axisSize[ i*3 + 1 ] );
axis.setDepth( axisSize[ i*3 + 2 ] );
// 色を設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor( axisColor[i] );
axis.setMaterial( material );
root.getChildren().add( axis );
}
// ビルボードを作成
BillBoard billBoard = new BillBoard( camera );
Image img = new Image( new File( "img/chara_one.png" ).toURI().toString() );
billBoard.setImage( img );
root.getChildren().add( billBoard );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
// カメラ移動のアニメーション・クラスを作成
AnimationTimer timer = new AnimationTimer()
{
private long startTime = 0;
private final long CYCLE_TIME = 5000; // ミリ秒
@Override
public void handle(long t)
{
// 開始時間を取得
if( startTime == 0 ){ startTime = t; };
// 回転角度を計算
long time = startTime - t; // 経過時間
long cycleTime = CYCLE_TIME * 1000000L; // アニメーションのサイクル
double percent = (double) ( time % cycleTime ) / cycleTime; // アニメーションの進捗度合
double angle = 360.0 * percent;
// カメラ設定(x軸周りで回転)
camera.getTransforms().clear();
camera.getTransforms().add( new Rotate( angle , -camera.getTranslateX() , -camera.getTranslateY() , -camera.getTranslateZ() , Rotate.X_AXIS ) );
camera.getTransforms().add( new Rotate( 30 , -camera.getTranslateX() , -camera.getTranslateY() , -camera.getTranslateZ() , Rotate.Y_AXIS ) );
camera.getTransforms().add( new Translate( 0 , 0 , -10 ) );
}
};
// 画面クリックでアニメーション開始
primaryStage.addEventHandler( MouseEvent.MOUSE_CLICKED , e -> timer.start() );
}
/**
* ビルボードを表すクラス
* @author tomo
*
*/
public class BillBoard extends MeshView
{
// ビルボードを関連付けるカメラ
private Camera camera;
/**
* カメラを指定してビルボードを作成
* @param camera ビルボードを関連付けるカメラ
* @throws NonInvertibleTransformException
*/
public BillBoard( Camera camera ) throws NonInvertibleTransformException
{
// カメラを設定付ける
this.camera = camera;
// 面メッシュを作成
setMesh( createTriangleMesh() );
// 空のマテリアルを設定
setMaterial( new PhongMaterial() );
// カメラが未指定の場合、
// 普通のメッシュとして表示するため、続く処理を行わない
if( camera == null ){ return; }
// 面メッシュをカメラに対して垂直に設定(初期設定)
Transform cameraTrans = this.camera.getLocalToSceneTransform();
Affine billTrans = new Affine( cameraTrans.getMxx() , cameraTrans.getMxy() , cameraTrans.getMxz() , 0 ,
cameraTrans.getMyx() , cameraTrans.getMyy() , cameraTrans.getMyz() , 0 ,
cameraTrans.getMzx() , cameraTrans.getMzy() , cameraTrans.getMzz() , 0 );
this.getTransforms().clear();
this.getTransforms().add( billTrans );
// カメラが移動した場合にも
// 面メッシュがカメラに対して垂直になるように設定
this.camera.localToSceneTransformProperty().addListener( ( ov , old , current) ->
{
Transform cameraT = this.camera.getLocalToSceneTransform();
Affine billT = new Affine( cameraT.getMxx() , cameraT.getMxy() , cameraT.getMxz() , 0 ,
cameraT.getMyx() , cameraT.getMyy() , cameraT.getMyz() , 0 ,
cameraT.getMzx() , cameraT.getMzy() , cameraT.getMzz() , 0 );
this.getTransforms().clear();
this.getTransforms().add( billT );
} );
}
/**
* ビルボードに表示する画像を指定
* @param img
*/
public void setImage( Image img )
{
// マテリアルに画像を指定
PhongMaterial material = (PhongMaterial) this.getMaterial();
material.setDiffuseMap( img );
this.setMaterial( material );
}
/**
* ビルボードに表示されている画像を取得
* @return
*/
public Image getImage()
{
// マテリアルから画像を取得
PhongMaterial material = (PhongMaterial) this.getMaterial();
return ( material == null )? null : material.getDiffuseMap();
}
/**
* トライアングル・メッシュを作成
*
* 【メッシュ】
* p0┏━┓p3
* ┃\┃
* p1┗━┛p2
*
* 【テクスチャ】
* t0┏━┓t3
* ┃ ┃
* t1┗━┛t2
*
* @return
*/
private Mesh createTriangleMesh()
{
// メッシュを作成
TriangleMesh mesh = new TriangleMesh();
float[] points = { -1 ,-1 ,0 , // p0
-1 ,1 ,0 , // p1
1 ,1 ,0 , // p2
1 ,-1 ,0 }; // p3
float[] texCoords = { 0 , 0 , // t0
0 , 1 , // t1
1 , 1 , // t2
1 , 0 }; // t3
int[] faces = { 0 , 0 , 1 , 1 , 2 , 2,
2 , 2 , 3 , 3 , 0 , 0 };
mesh.getPoints().addAll( points );
mesh.getTexCoords().addAll( texCoords );
mesh.getFaces().addAll( faces );
return mesh;
}
}
}
◇リソース
(chara_one.png)
◇実行結果
◇解説
サンプルコードでは、ビルボードを表すクラスをMeshViewを継承して作成している(122行目~230行目)。コンストラクタでは、面メッシュと空のマテリアルを作成し、面をカメラ方向に向けている(134行目~153行目)。さらにカメラの動きに連動して面方向を変えるため、camera.localToSceneTransformProperty(シーンに対するカメラの変換行列プロパティ)に対してバインディングを利用して面方向を変える設定を行っている。
また、ビルボードに表示する画像についてはα値が適用される。このためサンプルのように透過png等を利用すると、四角形のビルボードであってもα値にあわせてクリッピングされる。
■ 参照
- [エフェクト講座] ビルボードって何?