今回はJavaFXにてパーティクル表現を行う方法を見ていく。理想としては3次元パーティクルをシーングラフに追加できればよいのだが、とりあえず2次元でCanvas上にパーティクルを描く方法を見ていく。内容的にはSwingでも同様のことが行える。ちなみに、シーングラフにパーティクルを追加した場合には処理が重くなるとのこと(※参照1)なので、あまり現実的ではない模様。
■ パーティクルとは
パーティクルはその名のとおり粒子のことで、3Dレンダリングの世界では砂のような細かな物体を大量に扱う場合に利用する。水のような液体もプログラム上ではパーティクルとして捉えて物理演算を行うことが一般的であるらしい。JavaFXではパーティクルを扱う標準的な手法は提供されていないため、それに近いことをやってみる。今回は以下のような輝度を持った2次元のパーティクルを作成する。
■ サンプルプログラム
上記のように輝度を持つパーティクル表現を行うサンプルプログラムを以下に示す。サンプルでは100段階の輝度レベルを持つパーティクルを定期的に生成している。パーティクル生成時にランダムな向きの速度と輝度を与え、輝度に関しては時間ともに輝度レベルが減衰していく。
◇サンプルコード
package application_fx_functional;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BlendMode;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TestParticle2 extends Application
{
// 定数
private final int BRIGHT_MAX = 100; // パーティクルに設定可能な最大輝度レベル
private final int BRIGHT_SPAN = 50; // パーティクルの輝度レベル間のギャップ
private final double BRIGHT_DECAY = 0.32; // パーティクル輝度の減衰率
private final long PARTICLE_GENERATE = 50; // パーティクル発生の間隔(ミリ秒)
private final int IMAGE_WIDTH = 32; // パーティクル画像の幅
private final int IMAGE_HEIGHT = 32; // パーティクル画像の高さ
public static void main(String[] args)
{
launch( args );
}
@Override
public void start(Stage primaryStage) throws Exception
{
// 変数の初期化
int width = 600;
int height = 400;
// シーングラフを作成
Group root = new Group();
// キャンバスを作成
Canvas canvas = new Canvas( width , height );
root.getChildren().add( canvas );
// シーンを作成
Scene scene = new Scene( root );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
// グラフィクス・コンテキストを取得し、
// 初期表示画面を描画
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill( Color.BLACK );
gc.fillRect( 0 , 0 , width , height );
gc.setFill( Color.WHITE );
gc.fillText("画面をクリックするとアニメーションが再生されます", 0, 20 );
// 画像ファイルを準備
Image[] images = preCreateImage();
// 画像カタログ表示
for( int i=0 ; i<images.length ; i++ )
{
int row = i % 10 + 1;
int column = i / 10;
gc.drawImage( images[i] , column*IMAGE_WIDTH , row*IMAGE_HEIGHT );
}
// 初期パーティクルを作成
List<Particle> particles = new ArrayList<Particle>();
for( int i=0 ; i<5 ; i++ ){ particles.add( new Particle( width/2 , height/2 ) ); }
// アニメーション開始
AnimationTimer timer = new AnimationTimer()
{
private long startTime =0;
private long beforeTime =0;
private long count =0;
@Override
public void handle(long t)
{
// 開始時間を取得
if( startTime == 0 ){ startTime = t; };
// FPSを計算
long flap = t - beforeTime;
double fps = 1000000000L / flap;
// パーティクルを追加
if( count != ( t - startTime ) / ( PARTICLE_GENERATE * 1000000L ) )
{ for( int i=0 ; i<5 ; i++ ){ particles.add( new Particle( width/2 , height/2 ) ); } }
// ライフサイクルを終えたパーティクルを削除
particles.removeIf( p -> p.getBright() == 0 );
// パーティクル位置を更新
particles.stream().parallel().forEach( p -> p.update() );
// 画面を初期化
gc.setGlobalBlendMode( BlendMode.SRC_OVER );
gc.setFill( Color.BLACK );
gc.fillRect( 0 , 0 , width , height );
// FPSを描画
gc.setFill( Color.WHITE );
gc.fillText( "FPS :" + fps , 0, 20);
// パーティクル数を描画
gc.setFill( Color.WHITE );
gc.fillText( "particle :" + particles.size() , 0, 40);
// パーティクルを描画
gc.setGlobalBlendMode( BlendMode.ADD );
particles.stream().forEach( p -> gc.drawImage( images[ (int)p.getBright() ] , p.getX() , p.getY() ) );
// カウントアップ
count = ( t - startTime ) / ( PARTICLE_GENERATE * 1000000L );
beforeTime = t;
}
};
// 画面クリック時にアニメーションを開始
primaryStage.addEventHandler( MouseEvent.MOUSE_CLICKED , e -> timer.start() );
}
/**
* 画像を作成
* @return
*/
public Image[] preCreateImage()
{
// 作業変数を準備
List<Image> images = new ArrayList<Image>();
WritablePixelFormat<IntBuffer> format = WritablePixelFormat.getIntArgbInstance();
// 画像を作成
for( int i=0 ; i<BRIGHT_MAX ; i++ )
{
// 輝度を計算
double bright = i * BRIGHT_SPAN;
// 画像を作成
WritableImage img = new WritableImage( IMAGE_WIDTH , IMAGE_HEIGHT );
PixelWriter writer = img.getPixelWriter();
int[] pixels = new int[ IMAGE_WIDTH * IMAGE_HEIGHT ];
// ピクセル配列を作成
for( int y = 0 ; y < IMAGE_HEIGHT ; y++ )
for( int x = 0 ; x < IMAGE_WIDTH ; x++ )
{
// インデックスを計算
int index = ( y * IMAGE_WIDTH ) + x;
// RGB値を初期化
int a = 255;
int r = 0;
int g = 0;
int b = 0;
// ピクセルを設定
// 距離を計算
// distance = 平方根(x^2 + y^2)
double disX = Math.abs( x - IMAGE_WIDTH / 2 );
double disY = Math.abs( y - IMAGE_HEIGHT / 2 );
double distance = Math.sqrt( disX * disX + disY * disY );
// 光の強さを計算
// RGB値を再計算
a += 0;
r += bright / distance / distance / 1.5;
g += bright / distance / distance ;
b += bright / distance / distance ;
// 上限値を設定
if( r > 255 ){ r=255; }
if( g > 255 ){ g=255; }
if( b > 255 ){ b=255; }
// RGB値を設定
pixels[ index ] = ( a << 24 ) | ( r << 16 ) | ( g << 8 ) | b ;
}
// ピクセル配列から画像を作成
writer.setPixels(0 , 0 , IMAGE_WIDTH , IMAGE_HEIGHT , format, pixels, 0 , IMAGE_WIDTH );
// リストに画像を追加
images.add( img );
}
// 配列に変換
return images.toArray( new Image[0] );
}
/**
* パーティクル(1つ)をあらわすクラス
* @author tomo
*
*/
public class Particle
{
private double bright;
private double x;
private double y;
private double vecX;
private double vecY;
public Particle( double x , double y )
{
this.x = x;
this.y = y;
// ランダムな輝度を設定
int half = BRIGHT_MAX / 2;
bright = half + half * Math.random();
// ランダムな速度を設定
double radian = Math.toRadians( Math.random() * 360 );
vecX = Math.sin( radian ) * 1;
vecY = Math.cos( radian ) * 1;
}
/**
* 描画を更新する
* @param now
*/
public void update()
{
// 移動
x += vecX;
y += vecY;
// 減衰
bright -= BRIGHT_DECAY;
if( bright < 0 ){ bright = 0; }
}
/* 以下は単純なgetter/setter */
public double getBright() {
return bright;
}
public void setBright(double bright) {
this.bright = bright;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
public double getVecX() {
return vecX;
}
public void setVecX(double vecX) {
this.vecX = vecX;
}
public double getVecY() {
return vecY;
}
public void setVecY(double vecY) {
this.vecY = vecY;
}
}
}
◇解説
サンプルプログラムでは、単純にパーティクルを個別に1つ1つ描画している。パーティクルの位置情報はParticleクラスに持たせており、1インスタンスが1パーティクルに相当する。内部にx軸方向とy軸方向の速度や輝度などの情報を持たせており、Particle::update関数で時間を進め、パーティクルの位置と輝度を更新している。
サンプルプログラムの流れは以下のとおりである。3~5の手順はAnimationTimerを用いてループ処理を行っており、パーティクル描画では加算ブレンドを指定(128行目)することで光っているように見せている。
- パーティクルを描写するCanvasを準備(49行目~50行目)
- パーティクル画像を作成(70行目)
- パーティクルを定期的に生成(82行目~83行目、104行目~105行目)
- 生成済みのパーティクル位置を変更(111行目)
- 生成済みのパーティクルをすべて描画(128行目~129行目)
■ 参照
- We Code 4 Fun(Particle Systems)