JavaFX の標準機能だけでシンプルな 3D トイピアノをつくろう

JavaFX

このエントリーは、JavaFX Advent Calendar 2014, 11日目です。

昨日は @toruwest さんの「JavaFXのTreeViewでアニメーションしてみる」でした。

明日は @skrb さんの「Java Advent Calendarと一緒になにか書きます」です。

今年も残り少なくなってきていろいろと忙しくなってきました。

そんな今日この頃、何故か「猫ふんじゃた」のメロディがマイブームとなってきました。

忙しいのなら猫の手も借りたい状況なのに踏んじゃったらいけないのにね!

と言うわけで「猫ふんじゃった」を弾きたくなって JavaFX で ToyPiano アプリケーションを作ることにしました。

JavaFX のサンプルに Xylophone という 3D アプリケーションがあります。

それを参考に JavaFX 8 の標準機能だけを使って作ってみます。

今回作るのはトイピアノなので和音にも対応させるためにタッチインタフェースを利用します。

よって動作可能なのはタッチパネル対応の OS 限定となってしまいます。

もちろん、タッチパネルは必須です。(^^;

せっかく 3D アプリケーションにするのだから鍵盤もそれなりに動かしてみます。

全てを書こうとすると凄く長くなってしまいますので JavaFX 3D API だけを取り上げます。

タッチインタフェースやアニメーションタイマーの使用については割愛させていただきます。

と言っても他の部分は特に難しいことはしてないので!

完成版は次の動画のようになります。

 

それでは、さらっと JavaFX 3D API について調べてみましょう。

2D と 3D の違いは何か?

おおざっぱに言うと 2D の座標に Z 軸が追加され、ライトやカメラを意識しなければならなくなった。

その代わりに表現力が豊になった。

本当におおざっぱですがそんなもんです。

では、JavaFX ではじめから用意されている 3D オブジェクト(Primitive Shapes)を表示させるシンプルなプログラムを見てみましょう。

JavaFX では三つの 3D オブジェクトがはじめから javafx.scene.shape パッケージに定義されています。

Box

Cylinder

Sphere

上記のプログラムはそれらを下図のように表示させています。

1

 

これら以外は自分で TriangleMesh / MeshView を駆使して 3D オブジェクトを作り上げるか、モデルインポーターを使用するかしなければならないでしょう。

さて、このプログラムはどのように理解すればいいのでしょうか?

順番にみていきましょう。

    final Box box = new Box(100.0d, 100.0d, 100.0d);

これは、辺の長さが横、縦、奥行きそれぞれ 100 の長さの立方体のオブジェクトを作っています。

    final Cylinder cylinder = new Cylinder(50.0d, 100.0d);

こちらは、半径 50、高さ 100 のシリンダーオブジェクトを作ってます。

    final Sphere sphere = new Sphere(50.0d);

最後に半径 50 の球体オブジェクトを作ってます。

はじめから定義されているのでお手軽に作れます。

これら 3D オブジェクトはこのままでは全て座標原点を中心に配置され重なってしまいます。

なので次のように移動させています。

    // box を左へ (-200) 移動 (X 軸)
        box.setTranslateX(-200.0d);
       
        // cylinder を右へ (200) 移動 (X 軸) そして上へ (-100) 移動 (Y 軸)
        cylinder.setTranslateX(200.0d);
        cylinder.setTranslateY(-100.0d);

javafx.scene.Node クラスの setTranslateX(double value), setTranslateY(double value), setTranslateZ(double value) と言う便利な移動メソッドがあります。

それぞれ X 軸、Y 軸、Z 軸方向へ移動できます。

ここで注意が必要なのは Y 軸 です。

最初にも説明したように JavaFX の 3D 座標は 2D のものに Z 軸が加えられたものなので上方向に移動させる場合は、

setTranslateY(double value) の引数の値は負の値になります。

次はカメラについて見てみます。

カメラには PerspectiveCamera (透視投影カメラ) と ParallelCamera (直投影カメラ) の二種類があります。

今回は PerspectiveCamera を使用します。

        // 透視投影カメラ
        final PerspectiveCamera cam = new PerspectiveCamera(true);
        // Field of View
        cam.setFieldOfView(45.5d);
        // Clipping Planes
        cam.setNearClip(1.0d);
        cam.setFarClip(1_000_000.0d);

PerspectiveCamera cam = new PerspectiveCamera(true); の引数は fixedEyeAtZeroプロパティで true に設定することによって

カメラの位置が原点に配置されます。

ちなみにこの場合の原点の位置は画面のど真ん中です。

これを true に設定することによって容易にカメラを動かすことが可能となります。

カメラを原点位置に生成することは上記のように簡単にできます。

次にカメラの写り具合を設定します。

視野角の設定は以下のようにします。

   cam.setFieldOfView(45.5d);

引数の値、視野角 45.5 度で設定しました。何処かで OpenGL の記事か何かでこれくらいが良いと見たような記憶があるので(^^;

次にカメラからどれくらいの距離を写すのか設定します。

カメラからこれだけ離れていれば写すよってのが

    cam.setNearClip(1.0d);

で、引数の値 1.0 で設定されてます。これ以上近い物は写りません。

カメラからどれだけ離れているところまで写すかは

    cam.setFarClip(1_000_000.0d);

引数の値、1_000_000 まで写ります。

これ以上遠くは写りません。

これら設定はケースバイケースかお好みで!

カメラを配置、設定したのはいいけどこれでは原点配置なので sphere のど真ん中なので残念な状態です。

そこでカメラを移動させてみます。

Z 軸手前方向にカメラを移動させます。

        // カメラを 500 後退させる。(Z 軸を -500)
        cam.setTranslateZ(-500.0d);

これで原点から 500 Z 軸方向に後退した位置にカメラを配置できました。

あとはこのカメラを Scene にセットすればいいだけです。

        scene.setCamera(cam);

このプログラムの実行結果から原点配置の状態でカメラのレンズは Z 軸、正の方向に向いていることが確認できました。

これで Primitive Shapes の生成と移動、PerspectiveCamera の生成と設定、移動、撮影ができるようになりました。

ここで ParallelCamera (直投影カメラ) を使うとどうなるか試してみましょう。

先ほどのカメラの定義とカメラの移動を次のように変更します。

        // 直投影カメラ
        final ParallelCamera cam = new ParallelCamera();

        // Field of View ありません!
        // cam.setFieldOfView(45.5d);
       
        // Clipping Planes
        cam.setNearClip(1.0d);
        cam.setFarClip(1_000_000.0d);

ParallelCamera (直投影カメラ)のコンストラクタに fixedEyeAtZeroプロパティを設定する引数はありません。

Field of View も設定できません。

直投影カメラの性質上必要ないのでしょう。

では、ParallelCamera (直投影カメラ)を使った場合プログラムの実行結果がどうなるのか確認してみます。

1a

Primitive Shapes の Sphere の中心が左上の角の部分( 2D の原点)となるように表示されています。

Sphere と ParallelCamera の位置情報を下記コードを書き加えて調べてみます。

        System.out.println(“Sphere の位置情報 ” + sphere.getBoundsInParent());
        System.out.println(“ParallelCameraの位置情報 ” + cam.getBoundsInParent());

実行結果で得られた値は次のようになります。

Sphere の位置情報 BoundingBox [minX:-50.0, minY:-50.0, minZ:-50.0, width:100.0, height:100.0, depth:100.0, maxX:50.0, maxY:50.0, maxZ:50.0]
ParallelCameraの位置情報 BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:0.0, height:0.0, depth:0.0, maxX:0.0, maxY:0.0, maxZ:0.0]

3D オブジェクトとカメラの座標の原点は同一ではないことが解ります。

デフォルトの 2D カメラってことですね。

Sphere と Cylinder と Box を移動させてみます。

        sphere.setTranslateX(scene.widthProperty().divide(2).doubleValue());    
        sphere.setTranslateY(scene.getHeight() / 2.0d);

        cylinder.setTranslateX(scene.widthProperty().divide(2).doubleValue() + 200.0d);
        cylinder.setTranslateY(scene.getHeight() / 2.0d);
       
        box.setTranslateX(scene.widthProperty().divide(2).doubleValue() – 200.0d);
        box.setTranslateY(scene.getHeight() / 2.0d – 100.0d);

プログラムの実行結果は下図のようになります。

1b

Sphere の位置情報 BoundingBox [minX:270.0, minY:130.0, minZ:-50.0, width:100.0, height:100.0, depth:100.0, maxX:370.0, maxY:230.0, maxZ:50.0]
ParallelCameraの位置情報 BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:0.0, height:0.0, depth:0.0, maxX:0.0, maxY:0.0, maxZ:0.0]

PerspectiveCamera (透視投影カメラ)とは違いすべてのオブジェクトが並行に表示されてます。

距離感というか奥行きが全然解らないですね。

ParallelCamera (直投影カメラ)も試したことだし次へ進みましょう。

3D オブジェクト(Primitive Shapes)を表示させることはできるようになったけど、

でも白黒なんていつの時代だ?っていうのはちょっとね・・・

とりあえず cylinder に色をつけてみましょう。

先ほどのプログラムで Primitive Shapes を生成したあとに下記コードを追加します。

        // フォンシェーディングを設定
        final PhongMaterial cylinderMaterial = new PhongMaterial();
        // 拡散光による色の設定
        cylinderMaterial.setDiffuseColor(Color.RED);
        // スペキュラカラー(反射光の色)の設定
        cylinderMaterial.setSpecularColor(Color.MAGENTA);
        // マテリアルを設定
        cylinder.setMaterial(cylinderMaterial);
        // ドローモードを設定
        cylinder.setDrawMode(DrawMode.FILL);

上記コードのコメントにあるように、フォンマテリアルオブジェクトを生成し、拡散光による色の設定、反射光による色の設定をします。

そしてそのマテリアルを cylinder オブジェクトに設定しています。

ドローモードも LINE とか設定できますが FILL を設定しています。

LINE はワイヤーモデルの状態です。

上記のようにマテリアルの設定は javafx.scene.paint.PhongMaterial クラスの setチョメチョメ() メソッドで設定します。

チョメチョメは何を設定するか解るように記述されています。(ごめんなさい。変な表現で)

ドローモードの設定は javafx.scene.shape.Shape3D クラスの setDrawMode(DrawMode value) です。

プログラムの実行結果は下図のようになります。

2

ちょっとこのままでは色のついてない残り二つが寂しのでそれらにも色をつけてみました。

ここで Sphere だけスペキュラパワーを設定してみました。

デフォルト値は 32.0d なので半分の 16.0d で設定しました。

        // フォンシェーディングを設定
        final PhongMaterial boxMaterial = new PhongMaterial();
        // 拡散光による色の設定
        boxMaterial.setDiffuseColor(Color.GREEN);
        // スペキュラカラー(反射光の色)の設定
        boxMaterial.setSpecularColor(Color.LIGHTGREEN);
        // マテリアルを設定
        box.setMaterial(boxMaterial);
        // ドローモードを設定
        box.setDrawMode(DrawMode.FILL);
       
        // フォンシェーディングを設定
        final PhongMaterial sphereMaterial = new PhongMaterial();
        // 拡散光による色の設定
        sphereMaterial.setDiffuseColor(Color.BLUE);
        // スペキュラカラー(反射光の色)の設定
        sphereMaterial.setSpecularColor(Color.LIGHTCYAN);
        // スペキュラパワーを設定
        sphereMaterial.setSpecularPower(16.0d);      
        // マテリアルを設定
        sphere.setMaterial(sphereMaterial);
        // ドローモードを設定
        sphere.setDrawMode(DrawMode.FILL);

このプログラムの実行結果は下図のようになります。

3

3D オブジェクトに色も付けることができたし次はライトを設定してみましょう。

ライトはアンビエントライトとポイントライトの二種類が用意されています。

アンビエントライトは環境光( 光源からの光が直接当たっていない部分を照らす光、物理的には光を受けている面からの散乱光である )のことです。

ポイントライトは点光源です。点光源を置いた位置から全方向に光りが伸びていきます。

その光りは距離が離れるにしたがって弱くなっていきます。

それでは先ほどのプログラムにこれら二つのライトを配置してみましょう。

        // アンビエントライト
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(Color.rgb(184, 134, 11, 0.5));
       
        // ポイントライト
        PointLight point = new PointLight();
        point.setColor(Color.GHOSTWHITE);
       
        // ポイントライトを移動
        point.setTranslateX(-800.0d);
        point.setTranslateY(-300.0d);
        point.setTranslateZ(-800.0d);

        root.getChildren().addAll(box, cylinder, sphere, ambient, point);

アンビエントライトとポイントライトを上記コードのように javafx.scene.LightBase クラスの設定メソッドを使い設定します。

それぞれの色を設定しているだけで特に難しいことはしなくていいようです。

ただポイントライトの配置が原点になってしまうのでポイントライトを移動させています。

移動させないと Sphere の中心位置に配置されるので Sphere が表示されなくなります。

これら二つのライトを配置したプログラムの実行結果は下図のようになります。

4

さて、ライトって一つだけしか配置できないのでしょうか?

ちょっと試してみましょう。

        // ポイントライト 2
        PointLight point_2 = new PointLight();
        point_2.setColor(Color.YELLOW);
       
        // ポイントライト 2 を移動
        point_2.setTranslateX(800.0d);
        point_2.setTranslateY(-300.0d);
        point_2.setTranslateZ(-800.0d);

        root.getChildren().addAll(box, cylinder, sphere, ambient, point, point_2);

プログラムの実行結果は下図のようになりました。

5

複数のライトが配置できるようです。

でも、特定の 3D オブジェクトにだけライトを当てるにはどうすればいいのでしょうか?

これも簡単に設定できます。

それでは先ほど追加したポイントライト 2 を Sphere だけに当てるようにしてみます。

下記コードのようにスコープを設定するだけです。

        point.getScope().add(sphere);

それでは確認のためにプログラムを実行してみます。

6

期待通りに Sphere だけポイントライト 2 が当たっています。

ライトを駆使すれば綺麗な絵を得ることができますね!

次は 3D オブジェクトを回転させてみましょう。

左に表示されている Box を左奥の辺を中心位置として水平に45度右回転させます。

次のコードを先ほどのプログラムに書き加えてください。

        // Box を水平45度右回転させる。回転の中心( ピボット )は左奥の辺とする
        box.getTransforms().setAll(new Rotate(45.0d, -50.0d, 0.0d, 50.0d, Rotate.Y_AXIS));

実行結果は下図のようになります。

7

javafx.scene.transform.Rotate インスタンス生成時に第一引数 回転角、第二引数、第三引数、第四引数はピボットの位置を X, Y ,Z 軸として設定します。

第五引数は回転軸を設定します。

それらを javafx.scene.Node クラスの getTransforms() で Box のトランスフォームオブジェクトを取得し setAll() メソッドで設定しています。

今回は setAll() メソッドを使いましたが次のような方法もあります。

    box.getTransforms().addAll(new Rotate(45.0d, -50.0d, 0.0d, 50.0d, Rotate.Y_AXIS));

 

これは今回目標とする ToyPiano アプリケーションで使うと問題が出るので前述の setAll() メソッド を使用します。

その問題は何かと言うとタッチプレスされたら鍵盤をある角度だけピボットを設定されたところから回転させ、

タッチリリースされたら動かした分だけ戻すという処理を add() メソッドを使って実装すると想定していた角度より大きく( 2倍 )動いてしまうことがあります。

何気に動かして動かした分戻したらいいやってのは駄目なこともあるんです。

このようなことを避けるため setAll() メソッドを使います。

これで 3D オブジェクトの移動、回転、そしてカメラとライトの設定ができるようになりました。

さて、これまでの実行結果を見て気が付いた方もいらっしゃると思うのですが 3D オブジェクトにアンチエイリアスが効いてないようです。

デフォルトではアンチエイリアスは無効となっているようなのでこれを有効にしてみましょう。

プログラムの Scene を生成するコードを次のように変更します。

        Scene scene = new Scene(root, 640.0d, 360.0d, true, SceneAntialiasing.BALANCED);
       
        scene.setFill(Color.BLACK);

何故かアンチエイリアスの設定をとる引数を使うと背景色の設定ができなくなります。

そのため別に背景色を設定するコードを付け加えました。

実行結果は下図のようになり今までとは違いエッジが滑らかに表示されます。

8

 

次に 3D オブジェクトの拡大やテクスチャの貼り付けなども説明したいところですが今回の ToyPiano アプリケーションでは使用していないので割愛させていただきます。

と言いつつ・・・簡単にどのようにするのか紹介します。

興味の無い方はこの部分は読み飛ばしてくださいませ。

ではよく見かけるサンプルを JavaFX で試してみましょう。

テクスチャを貼るには javafx.scene.paint.PhongMaterial クラスを使用します。

PhongMaterial インスタンスを生成するときにコンストラクタの引数として渡すか、

各種イメージを javafx.scene.paint.PhongMaterial クラスの設定メソッド

    setDiffuseMap(Image value)

    setBumpMap(Image value)

    setSpecularMap(Image value)

    setSelfIlluminationMap(Image value)

を使って設定するだけです。

非常にお手軽に設定できます。

このプログラムの実行結果は下図のようになります。

9

それぞれがどのように作用しているのか少しみておきましょう。

setDiffuseMap(Image value) だけを設定した場合プログラムの実行結果は下図のようになり、

でこぼこ感がまったくなくのっぺりしたようになります。

10

使ったイメージファイルはこれです。(サイズは違います)

11

 

setBumpMap(Image value) を設定することによりでこぼこ感を演出することができます。

イメージファイルはこれを使用します。

12

 

プログラムの実行結果は下図のようになります。

13

setSpecularMap(Image value) は反射光を制御するもので次のようなイメージファイルを使用します。

黒くなっている所は反射光を出さないようです。

14

では SpecularMap が設定されていない場合はどうなるのかみてみます。

setSpecularMap(Image value) を未設定としたうえでプログラムに反射光の設定を加えます。

        sphereMaterial.setSpecularColor(Color.RED);
       
        sphereMaterial.setSpecularPower(3.0d);

このプログラムの実行結果は下図のようになります。

赤い反射光がしっかりと表示されてます。

15

setSpecularMap(Image value) が設定されていれば反射光を制御可能となります。

setSpecularMap(Image value) を適用して実行すると下図のように反射光が適用されている部分が限定できます。

15b

ちょっと解りづらいけど灰色の部分だけ薄く反射されているのが確認できます。

解りやすくするために SpecularMap イメージファイルを下図のように反射ベース色を灰色から白色へと変更してみます。

15c

これで実行すると下図のようになります。

15d

SpecularMap を上手く活用すれば繊細な描画が可能となりそうです。

それでは DiffuseColor を設定してみます。

プログラムに次のコードを書き加えます。

        sphereMaterial.setDiffuseColor(Color.ORANGERED);

このプログラムを実行すると 設定した DiffuseColor オレンジレッドが適用され赤っぽい地球が表示されます。

面白いですね。

まるでガミラス帝国に攻撃されて放射能汚染された地球のようです。ww

16

最後に setSelfIlluminationMap(Image value) を試してみましょう。

これ使い道が良く解らないのですが名前からすると自発光のようです。

なのでライトを無しにして、マテリアルの設定は setSelfIlluminationMap(Image value) だけにしました。

        // SelfIlluminationMap
        sphereMaterial.setSelfIlluminationMap(
                new Image(this.getClass().getResource(“resources/earth_texture.jpg”).toExternalForm(),
                        8_192 / 2.0d,
                        4_092 / 2.0d,
                        true,
                        true
                )
        );

 

        root.getChildren().addAll(sphere);

 

実行結果は下図のようになりました。

17

やっぱり自発光の設定のようです。

発光色や強さの設定ができないかと調べてみたのですが解りませんでした。

以上テクスチャの貼り方を簡単に説明させていただきました。

ToyPiano アプリケーションではテクスチャは貼ってないので蛇足となってしまいましたがお気に入りのテクスチャを持っている方は貼ってお楽しみください。

蛇足ついでに 3D オブジェクトの拡大縮小もやっておきましょう。

Sphere がマウスクリックされた座標位置を取得しそれらの値を適当に加工して拡大縮小させてみます。

プログラムに下記コードを追加します。

        scene.setOnMouseClicked(event -> {
            PickResult pickResult = event.getPickResult();
            if (pickResult.getIntersectedNode() == sphere) {
                Point3D point3d = pickResult.getIntersectedPoint();
                if (point3d.getX() != 0.0d && point3d.getY() != 0.0d && point3d.getZ() != 0.0d) {
                    sphere.setScaleX(abs(point3d.getX() / 500.0d) * 1.2d);
                    sphere.setScaleY(abs(point3d.getY() / 500.0d) * 1.2d);
                    sphere.setScaleZ(abs(point3d.getZ() / 500.0d) * 1.2d);
                } else {
                    sphere.setScaleX(0.5d);
                    sphere.setScaleY(0.5d);
                    sphere.setScaleZ(0.5d);
                }
            } else {
                sphere.setScaleX(1.0d);
                sphere.setScaleY(1.0d);
                sphere.setScaleZ(1.0d);
            }
        });

 

プログラムの実行結果は下図のようになります。

18

19

PickResult クラスを使い Scene のクリックされたものが Sphere オブジェクトかどうか

getIntersectedNode() メソッドで判定し、そうであれば

getIntersectedPoint() メソッドで Point3D インスタンスを渡します。

Point3D クラスを使い、X, Y, Z 座標位置情報を取得してそれらの値の絶対値を Sphere の半径で割りそして 1.2 倍してます。

javafx.scene.Node クラスのメソッドを利用してそれぞれ X, Y, Z 座標値に適用してます。

setScaleX(double value)

setScaleY(double value)

setScaleZ(double value)

上記プログラムでは X, Y, Z 軸の何れかの座標位置が 0 なら拡大縮小を 0.5 倍するようにしています。

Sphere オブジェクトがクリックされてなければ拡大縮小は元に(1.0 倍)に戻します。

拡大縮小も簡単にできますね!

ついでだから回転させてみましょう。

Z 軸を 23.5 度傾けて反時計回りに回転させてみます。

プログラムに下記のようみ変更します。

実行結果は下の動画のようになります。

AnimationTimer で地球を 23.5 度傾けて反時計回りに回転させているコードは次の部分です。

               // 23.5度傾けて自転させる。
                sphere.getTransforms().setAll(new Rotate(23.5d, Rotate.Z_AXIS), new Rotate(-angle, Rotate.Y_AXIS));

これを次のように記述してしまうと思い通りにいきません。

                angleZ.set(23.50d);
                angleY.set(angle);

うっかりこれやってしまうと Z 軸が 23.5 度傾いてから水平に反時計回りに回ってしまいます。

これはこの後のカメラを動かす場合でも同じように注意が必要となります。

せっかくここまで試したことだしもう少しだけ寄り道をしましょう。

自転する地球の周りに月を公転させてみます。

ポイントライトは太陽の位置とします。(大きさや距離に正確な値は使ってないです。適当です。)

今までの技術でさらっとできてしまいます。

このプログラムの実行結果は次の動画のようになります。

 

この動画から解るように地球や月で太陽の光(ポイントライト)が遮られたときにできるはずの影ができません。

少し残念な結果となりました。

ちょっと脱線していまいましたが ToyPiano アプリケーションに必要な最後の機能をそろそろ紹介します。

カメラを動かす!

これができるようになったら ToyPiano アプリケーションで使っている 3D 関係の技術は全てあなたのものです。

32 鍵盤とみみっちいこと言わずに 88 鍵盤の本格的な 3D Piano アプリケーションだって作れるかもしれません。

それでは次のようなプログラムを組んでみましょう。

Cylinder を手前に45度傾けて、ピボットは最下部で一番手前の位置として表示させる。

これは今までの記事の中で簡単にできることが証明されています。

トグルボタンを適当に配置して、ボタンが押されたらカメラを Cylinder を中心に水平に回転させてみます。

もちろんカメラは常に Cylinder に向いてます。

これをアニメーション表示さてます。

トグルボタンを再度押してアニメーションを止めて初期状態に戻します。

この記事をここまで読んでくださった方は X, Z 軸の位置を計算して移動させて、カメラが Cylinder の中心に向かうように

Y 軸を回転させればいいだけじゃん。って思ったでしょう。

おそらく多くの方は次のようなプログラムを思いつくのではないかと思います。

このプログラムの実行結果は下図のようになります。

20

なんかとんでもないことになってますね。

動画はこちらです。

カメラを動かすとトグルボタンも平べったい平面上に配置されている状態なのが確認されました。

ちょっとこれはまずいです。

トグルボタンが押しにくいじゃありませんか!(そこか!?

ちなみにこのトグルボタンはちゃんと機能します。(^_^;)

特にこのプログラムを説明するところは無いのですが、何故か四つ目のポイントライトが効きません。

カメラが回転していることを解りやすくするために四方から PointLight を当てようとしたのですが・・・

PointLight の件はちょっと解らないので深く追求せずに肝心の問題に移ります。( ̄。 ̄;)

このように UI 用のオブジェクト(コントロール)などを今まで通りに撮して、3D オブジェクトを撮す場合に影響を及ぼさないようにするにはどうすればいいのか?

実はこの問題を簡単に解決するために特別な Node が用意されています。

javafx.scene.SubScene です。

これを使って今までの 2D 用と 3D 用を分離させてしまいます。

カメラも 3D 用に( SubScene 用 ) 別に一つだけ特別に用意されています。

それではこのプログラムを SubScene を使って期待通りに動くようにしましょう。

先ほどのプログラムと見た目は違うけどやってることは同じです。

コントロールの配置がこれで自在にできるので先ほどのプログラムとこの部分は随分違ってます。

また、Scene 用のカメラが定義されてないのですが心配には及びません。

デフォルトのカメラが利用されてコントロールは表示されます。

Cylinder を撮すために回転しているカメラは SubScene に設定された専用のカメラなので先ほどのプログラムのように UI (コントロール)まで撮しません。

実に合理的にこの問題を解決していますね。

それでは SubScene を使って改善されたプログラム見てみましょう。

SubScene って簡単に便利に使えますね!(^_^)

プログラムの実行結果は次のようになります。

21

 

ToyPiano アプリケーションを作るために必要な JavaFX 3D API はこれで全てです。

JavaFX は標準で 3D API を持っているからちょっとしたことなら簡単に使うことができます。

仕事では使う機会がないかもしれませんが息抜きに遊んでみるのもいいかもしれませんね。(^_^)

長くなってしまいますが鍵盤数を減らした完成版のプログラムを載せておきます。

私はスーパーサイヤ人並の素人なのでいつもながら汚いコードですが参考になれば幸いです。

間違い、アドバイス等は大歓迎ですのでお気付きの点があればご教示くださいませ。

最後まで読んでくださってありがとうございます。

感謝をこめて注意が必要なことを書いておきます。

タッチインタフェースを使っているので次の例外が投げられるかもしれません。

Exception in thread “JavaFX Application Thread” java.lang.RuntimeException: Too many touch points reported

これが投げられても動いていればいいのですがタッチが効かなくなる場合があります。(マウスは動きます)

Twitter でつぶやいたら @aoetk さんから次のようなアドバイスをいただきました。

「処理を非同期にするのも一つの手ですね。タッチだとこの辺がどうしてもシビアになりますねえ。

Windows Storeアプリ開発に使うWinRT APIでは、50ミリ秒以上掛かる処理は非同期にせよといわれているようです。」

ありがとうございました。(^_^)

早速イベント処理を下記のように非同期に変更してみました。

        keyboard_f.setOnTouchPressed(te -> {

            Task<Void> task = new Task<Void>() {

                @Override
                protected Void call() throws Exception {

                    recordingEvent(recording, eventRecording, actionEventTime, keyboard_f, te);

                    Platform.runLater(() -> {

                        pressedWhiteKeyBoard(f, keyboard_f, whiteKeyBoardDepth);

                    });

                    return null;
                }

            };

            executor_1.execute(task);

        });
       

残念ながら改善されませんでした。

何が悪いのか解らずどうしようって悩んでいたのですが今年の9月の末に購入した VAIO ノートで動かしたら全然問題なしでした。

プログラムを組んでいた PC だけなのでひょっとしたらハードの問題かもしれません。

タッチパネルの性能?

タッチパネルのドライバとか?

何はともあれ新しい方の PC で問題ないから良しとしましょう。(ヲヒ

もう一つ、タッチインタフェースを使っているのでピアノを演奏しているときにタッチドラッグが発生して不用意に動いてしまうことがあります。

これを防止するために苦肉の策として次のようなことをしました。

これが正しいかどうかは解りませんが何らかの処理が必要となります。

        // プレイバック以外のタッチドラッグを禁止させるために
        subScene.setOnTouchPressed(te -> {
            touchPressedCount++; // フィールドへアクセス 取扱注意
        });

        subScene.setOnTouchReleased(te -> {
            touchReleasedCount++; // フィールドへアクセス 取扱注意
        });

    // 略

        subScene.setOnMouseDragged(event -> {
            if (touchPressedCount == touchReleasedCount || activePlayback) {
                angleX.set(anchorAngleX – (anchorY – event.getSceneY()));
                angleY.set(anchorAngleY + anchorX – event.getSceneX());
            }
        });

3D 同様タッチインタフェースを使った解説が少ないのでこういったときの定番処理というのが良く解りません。

先日もマウススクロールによるカメラポジションの変更で随分悩みました。

JavaFXでマウスホイールを使ってのズームインズームアウトは可能か?

タッチインタフェースの完全無欠の使い方を知りたい今日この頃です。

あと、カメラの移動、回転なんですがこのプログラムでは、見下ろし角度(X 軸)と中心への角度(Y 軸)でカメラを回転させています。

               // カメラ移動及び回転
                xPos.set(Math.sin(Math.toRadians(azimuth)) * radius);
                zPos.set(-1.0d * Math.cos(Math.toRadians(azimuth)) * radius);
                yPos.set(heightProperty.getValue());
                subCam.getTransforms().setAll(new Rotate(-1.0d * azimuth, Rotate.Y_AXIS), new Rotate(-1.0d * downAngle, Rotate.X_AXIS));

このコードの最後の行でカメラを回転させています。

ここで最後の行を次のようなコードを書くとはまります。

        ryPos.set(-1.0d * azimuth);
        rxPos.set(-1.0d * downAngle);

理屈的には同じように感じるのですがまったく違います。

残念ながらとんでもないカメラワークとなってしまいます。

見下ろし角をつけずに Y 軸だけ回転させるのであれば次のコードで問題ありません。

               // カメラ移動及び回転
                xPos.set(Math.sin(Math.toRadians(azimuth)) * radius);
                zPos.set(-1.0d * Math.cos(Math.toRadians(azimuth)) * radius);
                yPos.set(heightProperty.getValue());
                ryPos.set(-1.0d * azimuth);

回転させるコードを書くときは注意が必要です。

あとはいたってシンプルなものなので作って遊んでみるのもいいかもしれません。

実際に Box オブジェクトだけを使い移動と回転だけしかさせていません。

OneDrive にこのプログラムを置いてありますので遊んでみたい方は次のリンクからどうぞ。

https://onedrive.live.com/redir?resid=EAE4ED7554BC7AB6!34658&authkey=!AP29iHq_NpAEKiM&ithint=folder%2c

一応、何かトラブルが起こって損害が発生しても責任は一切負えませんので(^_^;)

/**
*
* @おまけのお知らせ

* SwingNodeとTextFlow
*
* JavaFX で英語力を補う
*
*/

最後にひと言

JavaFX 楽しい!


« »

Comment

Trackback

  1. [...] JavaFX の標準機能だけでシンプルな 3D トイピアノをつくろう [...]

Leave a Reply

* が付いている項目は、必須項目です!

次の HTML タグと属性を利用できます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

*

Trackback URL