JavaFXでバンプマッピング

このEntryは JavaFX Advent Calendar の最終日です。今年は誰も落とさずに、Adventしました!!すばらしい!!!!

昨日は @masanori_msl さんの JavaFX + Apache POIでSpreadsheet操作 でした。

さて、バンプマッピングです。

なぜ、バンプマッピングかというと、JavaFX Advent Calendar の 19 日目のひらおかさんの JavaFX で小惑星を描いてみよう で、私が Twitter で次のようにコメントしたからです。

とコメントしたのにもかかわらず、バンプマッピングのことってどこかに書いたことないなぁと思い出したわけです。

バンプマッピングとは

バンプ (bump) はデコボコのことです。ようするに、3D のオブジェクトの表面を擬似的にデコボコに描画することをいいます。

あくまでも擬似的なので、ほんとにデコボコを描くわけではないのですが、意外に使えるのです。

ちなみに、ほんとにデコボコを作るマッピングとしては、ディスプレイスメントマッピングというものがありますが、JavaFX ではサポートしていないのです ><

で、バンプマッピング

バンプマッピングにもいろいろあるらしいのですが、もっとも一般的で JavaFX でもサポートしている法線ベクトルを使用する方法を紹介します。

物体の表面の接線に直交している法線というのものを考えます。高校の数学でも法線出てきているので、思い出してね。

法線は英語だと Normal といいます。

その物体に当たった光は、その法線との入射角と同じ角度で反射します。

表面が均一だと、法線の向きもそろうのでキレイに反射します。鏡の表面のようなものです。でも、表面が荒れていて乱雑だと、法線の向きもバラバラになるので、反射光もどこに向かうか分からなくなります。すると、表面がマッドな感じになるわけです。

一般的には法線はベクトルとして扱います。3D CG をやっているとよく出てくる、法線ベクトルというやつです。

前述したように法線は表面の接面に直交したベクトルなのですが (3D なので接線ではなくて接面です)、これを意図的に変えてみようというのがバンプマッピングの基本的な考えです。

どういうことかというと、表面が均一でも下の図のように法線を違う方向にあえて向けてしまうのです。

すると、人間はあたかもそこにデコボコがあるかのように認知してしまうわけです。上の絵の場合、突起ですが、へっこみも同じように法線を内向きにすれば可能です。

でも、法線ベクトルを変更しているだけなので、本当にデコボコがあるわけではありません。あくまでも擬似的なものです。

バンプマッピングの準備

さて、バンプマッピングをやりたいわけですが、それに先駆けてやらなければならないことがあります。法線ベクトルを先に準備しなければならないということです。

オブジェクトにテクスチャを貼るように、法線ベクトルを表したマップを準備するのです。

マップの各ピクセルが法線ベクトルを表すようにします (厳密にはテクスチャと同じように、UV マッピングするので、ピクセルではないのですが...)。

では、どうやって各ピクセルで法線ベクトルを表すかというと、RGB の値にベクトルの x 座標 y 座標 z 座標を割り当ててしまいます。

といっても、そんなの作れないと思いますよね。実際には、ツールで作ってしまいます。

ここでは Photoshop を使った方法を紹介します。

今回はサンプルとして球に火星のテクスチャを貼る場合を考えてみます。火星の地形図は WikipediaGeoTemplate/mars を使用しました。

https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Mars_G%C3%A9olocalisation.jpg/720px-Mars_G%C3%A9olocalisation.jpg

まずはこのイメージを Photoshop に読み込みます。そして、メニューバーの [フィルタ] - [3D] - [法線マップを生成] を選択します。

すると、下図のようなダイアログが表示されます。これは生成した法線マップでバンプマッピングしたものです。これで、法線マップがちゃんと使えそうかどうかを確認します。

ここでは、球に貼ろうとしているので、そのままデフォルトでかまいません。もし、違う形状に貼ろうとするのであれば、左下の [オブジェクト] を変更します。

他のパラメータは基本的には触らなくても大丈夫です。

これで、[OK] すると、元々の表示が紫主体の表示に変化しているはずです。後はこれを保存するだけです。

昔の赤青フィルタで作った 3D の映画のようですね。

他のツールでも同じように作成できるはずです。フリーのツールもあるので、ググってみてください。私はもっぱら Photoshop を使っているので、他のツールのどれがいいのかよく分からないのですが ^ ^;;

さて、これで法線マップができました。

JavaFXバンプマッピングをする

準備はできたので、JavaFXバンプマッピングをしてみましょう。

といってもそんなに難しいことではありません。JavaFX でテクスチャを貼る時に使用する javafx.scene.paint.PhongMaterial クラスを、バンプマッピングでも使用します。

    PhongMaterial material = new PhongMaterial();

    // テクスチャの設定
    material.setDiffuseMap(new Image(テクスチャの画像のURL文字列));

    // バンプマッピングの設定
    material.setBumpMap(new Image(法線マップ画像のURL文字列));

    // オブジェクトにマテリアルを設定
    mars.setMaterial(material);

setDiffuseMap メソッドがテクスチャで、setBumpMap メソッドが法線マップです。後はこれを 3D のノードにセットするだけです。

上のコードでは変数 mars が Sphere クラスのオブジェクトになっています。

これでおしまい。準備の方が大変なぐらいですね。

バンプマッピングをしたものと、していないものを比較して見ましょう。

左がバンプマッピング有り、右が無しです。

テクスチャを貼っただけでも何となくデコボコに見えますが、バンプマッピングをするとさらにデコボコが大きくなるような感じです。微妙といえば微妙かもしれませんが...

また、本当にデコボコを作っているわけではないので、縁の部分などはアラが見えてしまいます。それでも、十分使えると思いませんか。

というわけで、JavaFXバンプマッピングでした。

ソースは gist にあげてあります。

JavaFX Bump Mapping Demo

JavaFXのGUI構築ツール、Scene BuilderでFXML編集

このEntryは JavaFX Advent Calendar の 22 日目です。

昨日は @fukai_yas さんの ScalaFXのCell描画を実装する でした。

明日は kimukou さんです。

さて、Scene Builder ですが、ここでは解説しません。

というのも、今日公開された ITpro の 最新Java情報局 で書いているからです。

JavaFXのGUI構築ツール、Scene BuilderでFXML編集

JavaFX の解説はこれで 4 回目。ぜひ、1 回目から読んでください!!!!

ということで、ステマかつ手抜きのエントリーでしたw

Sooner or Later

このEntryは JavaFX Advent Calendar の16 日目です。

昨日は @khasunuma さんの JavaFX から Payara Micro を呼び出す際の注意点 でした。

明日は @nodamushi さんです。

そして、この Entry は Java Puzzlers Advent Calendar 2016 の 16 日目でもあります。

昨日は @khasunuma さんの Puzzle : String concatination でした。

そして、明日も @khasunuma さんです。

さて、JavaFX で Puzzle を書くわけですが、GUI は出てきません。

GUI の機能ではないのですが、JavaFX で重要な機能の 1 つにバインドがあります。みなさん、バインド使ってますか?

バインドは JavaFX のプロパティ間で値を自動的に同期させるための機構です。

たとえば、以下のコードではどうでしょう。

    IntegerProperty x = new SimpleIntegerProperty(0);
    IntegerProperty y = new SimpleIntegerProperty();
    y.bind(x);

    x.set(10);
    System.out.println(y.get());

JavaFX ではプロパティを なんちゃらProperty というクラスで表します。この なんちゃらProperty クラスを使うと、バインドももれなく着いてくるというわけです。

で、y は x にバインドしているので、x が変更されると y も勝手に値が更新されます。

なので、このコードを実行すると 10 が出力されます。

バインドは自動で同期してくれるだけではなく、バインドが遅延処理されることも特徴です。

上のコードだと、x が変更されたら、y が変更されるのではありません。y を使用する時に、x が変更されているかどうかをチェックします。この遅延処理によってムダな同期処理を減らすことができるのです。

さて、ここからが Puzzle です。

次のコードを実行したらどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
        y.addListener((observable, previous, present) -> {
            System.out.println("Change");
        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

他の Puzzlers Advent の人はちゃんと 4 択にしているんですけど、Java Puzzlers の本はそうでないんですよね。 4 択にするのは、あくまでもプレゼンテーションのためです。

なので、ここでは選択肢は出しません。実行したら、どうなるでしょうか?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

答えは、次のようになります。

Change
Change
Change
Change
20

たぶん大方の人は、こうなると想像していたと思います。パズルにもなにもなっていないように感じられるかもしれません。

この問題は JavaFX をある程度知っている人が陥るパズルになっているのです。

試しに、次のコードではどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
//        y.addListener((observable, previous, present) -> {
//            System.out.println("Change");
//        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

先ほどのパズルのうち、一方の addListener メソッドをコメントアウトしただけです。

実行すると、次のようになります。

Change
20

Change が出力されるのは、2 回だと思いませんでしたか?

ところが、Change が出力されるのは、1 回です。

ラムダ式を使っているので、分かりにくくなっているのですが、x に登録しているリスナはそれぞれインタフェースが異なります。

引数が 3 つのラムダ式が ChangeListener インタフェース、1 つラムダ式が InvalidationListener インタフェースです。

ChangeListener インタフェースは、Java Beans の PropertyChangeListener インタフェースと同様に、プロパティの値が変更されたら、コールされます。

一方の InvalidationListener インタフェースはプロパティが invalid になった時にコールされます。これはどういうことかというと、遅延処理に関係しているわけです。

プロパティの使用時にプロパティの値が valid かどうかを調べて、invalid だったらコールされるのです。

上のコードでは y が使用されるのは、最後の出力の時だけなので、その時に valid かどうかチェックされます。

x は 2 回変更されていますが、valid かどうかのチェックはその後に行うので、結果的に 1 度しかコールされません。

ところが、ChangeListener インタフェースは異なります。バインドされているターゲットの値が変更されてもコールされます。つまり、その都度、値が変化しているかどうかチェックするわけです。

問題は、2 種類のリスナを登録するとどうなるかということです。

バインドのターゲットの値が変更されたら、ChangeListener インタフェースのメソッドがコールされます。この時に、古い値と新しい値を引数に渡します。このプロパティの値を取得するということは、プロパティを使用するということになります。

つまり、valid かどうかのチェックが行われてしまうのです。

ChangeListener インタフェースのメソッドは 2 回コールされるので、InvalidationListener インタフェースのメソッドも 2 回コールされてしまいます。

そして、最後の y.get() 時にも valid かどうかのチェックは行われるのですが、x.set(20) の後に変更はないので、値は valid です。このため、InvalidationListener インタフェースのメソッドはコールされないのです。

このパズルの教訓です。

  • バインドしているプロパティの値をチェックしたいからといって、安易に ChangeListener インタフェースを登録するのはやめよう

Puzzle のネーミング

Joshua Bloch のパズルのネーミングって、とってもウィットに富んでいます。中には日本ではよく分からない習慣や文化に基づいているものもあるので、全部理解できるわけではないのですが...

Java Puzzlers の翻訳をされた柴田さんもパズルのネーミングの意味を調べるのが大変だったらしいですし。

でも、こういうウィットやユーモアを含ませたネーミングというのは、とても重要だと思うわけです。

みんな、パズルを作る時にこういうところまで気にすると、もっとパズルがおもしろくなるのになぁと個人的には思っています。

今回の Sooner or Later ですが、日本語にすると「遅かれ早かれ」ということです。

遅いか早いかはよく分からないけど、最終的にはリスナのメソッドがコールされるよということです。もちろん、遅延処理が絡んでくるからこういうネーミングにしたわけですが、どうでしょうか?

Scene Builder 小ネタ 3つ

このEntryはJavaFX Advent Calendarの11日目です。

昨日は @Yucchi_jp さんの HitInfoを少しだけ… でした。

明日は @boochnich さんです。

すいません、小ネタです。

Scene Builder をビルドする

みなさん、Scene Builder使ってますか? 便利ですよね。

でも、Oracleバイナリパッケージを配布しなくなってしまったのが... めんどくさいんですかねぇ。まぁ、Gluonがバイナリパッケージを配布してくれているからいいのですが。

とはいえ、せっかくのOSSなのですから、ビルドしてみたいと思いませんか。

JavaFX、というかOpenJFXをビルドするにはいろいろと準備が必要なのでめんどうなんですけど、Scene Builderだけであれば簡単です。

さっそくやってみましょう。

なお、今回はJava SE 8用のScene Builderをビルドします。

まずは、OpenJFXをクローンします。残念ながら、Scene Builderだけをクローンすることはできないのです。ちなみに、OpenJDKが使っているのはMercurialです。

hg clone http://hg.openjdk.java.net/openjfx/8u-dev/rt

クローンが完了しましたか。そうしたら、rt/apps/scenebuilderを見てみましょう。SceneBuilderAppディレクトリがScene BUilder本体、SceneBuilderKitディレクトリがIDEに組み込むなどの用途で使用するライブラリになってます。

OpenJFXのビルドにはgradleを使うのですが... なんとSceneBuilderAppのディレクトリを見てみるとnbprojectというディレクトリがあります。

これが何を意味するかというと、NetBeans用のプロジェクトになっているということです。

さっそくNetBeansで読み込んでみましょう。

SceneBuilderApp単独ではビルドできないので、SceneBuilderKitも一緒に読み込みます。

後は、SceneBuilderAppプロジェクトでF6すれば、ビルドして実行します。

JavaFXのアプリケーションは実行するとJARファイルも作ってくれます。distディレクトリにSceneBuilderApp.jarファイルができているはずです。後は-jarオプションで実行すればOK。

java -jar SceneBuilderApp.jar

とはいえ、dist/libにあるSceneBuilderKit.jarも一緒に使ってます。なので、他の場所にコピーする時はSceneBuilderKit.jarも忘れずに。

ルートコンテナを変更する

たとえば、NetBeans で FXML を生成すると、勝手に AnchorPane をルートコンテナとする FXML を生成しますよね。「空のFXML」を生成するといっても、ルートコンテナだけは勝手に設定してしまいます。

でも、AnchorPane を使いたいわけじゃないんだよ、他のコンテナを使いたいんだよ、ということも多いのでは。

そんな時、どうするかというと、IDEテキストエディタで FXML を編集すればいいのですが... そんな時でも、Scene Builder を使いたいわけです。

Scene Builder には Wrap in という機能があります。これを使えば、ルートコンテナを変更することもできるのです。

Wrap in は任意のノードをコンテナで包み込む機能です。

たとえば、AnchorPane に Button が貼られているとします。この Button を、AnchorPane の直下ではなく、間にコンテナ、たとえば FlowPane を挟みたいような場合に使用します。

これをやるには、Wrap in したいノードを選択します。選択するのは Hierarchy の処でも、中央のエディタ部分でもどちらでも OK。

そして、右クリックでポップアップメニューを表示させ、[Wrap in] を選択します。すると、下の図のようにコンテナの一覧がサブメニューに表示されるので、使用したいコンテナを選択します。

ノードを直接右クリックしなくても、メニューバーの [Arrange] - [Wrap in] でもおなじことができます。

Wrap in すると、ノードとコンテナの間に、指定したコンテナが挟まります。

この機能、どんなノードに対してもできるので、コンテナに対してもできます。つまり、ルートコンテナでも OK ということです。

ルートコンテナに対して Wrap in をしても、ちゃんと名前空間の定義やコントローラクラスへのリンクが保ったままにしてくれます。

ただし、スタイルシートは引き継がないので、注意が必要です。

たとえば、次のような FXML があったとしましょう。

<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="ViewController">
    <stylesheets>
        <URL value="@view.css"/>
    </stylesheets>
</AnchorPane>

これを BorderPane で Wrap in すると、次のようになります。

<BorderPane xmlns="http://javafx.com/javafx/8.0.60" 
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="ViewController">
   <center>
      <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0">
          <stylesheets>
              <URL value="@view.css" />
          </stylesheets>
      </AnchorPane>
   </center>
</BorderPane>

CSS の view.css がルートコンテナに引き継がれていません。また、サイズは設定されていないのは、しかたないですね。

コントローラクラスの雛形を作る

「コントローラクラスの雛形を作るのにいい方法ないですか」と FB で質問されたのですが、意外にみなさん Scene Builder の機能を知らないのですね ^ ^;;

コントローラクラスの雛形を作るのは簡単。

メニューバーの [View] - [Show Sample Controller Skelton] を選択すれば OK です。

たとえば、某連載用に作った次の FXML でコントローラクラスを作ってみましょう。

<VBox alignment="CENTER" prefHeight="100.0" prefWidth="400.0" spacing="12.0" 
      xmlns="http://javafx.com/javafx/8.0.60" 
      xmlns:fx="http://javafx.com/fxml/1" 
      fx:controller="DictionaryController">
   <children>
      <HBox alignment="CENTER" spacing="20.0">
         <children>
            <TextField fx:id="keyField" prefColumnCount="20" promptText="Key" />
            <Button mnemonicParsing="false" onAction="#search" text="Search" />
         </children></HBox>
      <Label fx:id="valueLabel" />
   </children>
</VBox>

この FXML は fx:id で結びつけられた要素が 2 つ (keyField, valueLabel)、イベント処理が 1 つ (searchメソッド) あります。

では、[View] - {Show Sample Controller Skelton] を選択してみると...

というダイアログが表示されます。ファイルには落とせないので、左下の[コピー]を選択すれば、クリップボードに保存してくれるので、後はペーストするだけ。

でも、このスケルトン、間違っているんですよね ^ ^;;

何が違うかというと、ActionEvent の import 文が抜けてます。まぁ、import 文は IDE で補完してくれるから、問題ないといえば、ないのですが。

ちなみに、右下の [Full] を選択すると、コードが次のようになります。

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class DictionaryController {

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private TextField keyField;

    @FXML
    private Label valueLabel;

    @FXML
    void search(ActionEvent event) {

    }

    @FXML
    void initialize() {
        assert keyField != null : "fx:id=\"keyField\" was not injected: check your FXML file 'dictionaryView.fxml'.";
        assert valueLabel != null : "fx:id=\"valueLabel\" was not injected: check your FXML file 'dictionaryView.fxml'.";

    }
}

これだと、ちゃんと ActionEvent の import 文も含まれています。

ちなみに、initializeメソッドが Initializable インタフェースの initialize メソッドではないところに注意が必要ですね。

ところで、このダイアログ、違和感ないですか?

今の Scene Builder は L8N されていないのですが、なぜかこの [コピー] ボタンだけ日本語になっているのです。なんでなんでしょうね?

Displacement Map

ということで、JavaFX Advent Calendar も最後になりました。

昨日は id:kikutaro777 さんの Salesforce Driver by JavaFX でした。

さて、今年は JavaFX Advent Calendar を 6 回も書くことになってしまって、完全にネタ切れです。しかも、12 月だけで、3 回も講演をしに旅行に行っているので、いろいろと試す時間もなくて、つらいです ><

今日の内容も自分的には極められていないので、悔しいのですが、時間が迫っているので、しかたないのです。

で、今日の話題は Displacement Map です。

クラス的には javafx.scene.effect.DisplacementMap クラスです。

Displacement Map というのは、元々は 3D CG の言葉です。たとえば、惑星の表面のようにデコボコがあるような場合、従来はバンプマッピングという手法を使って、見せかけのデコボコを描いてました。

でも、見せかけだけなので、近くに寄ってきたりすると分かっちゃうんですよね。デコボコがないのが。そこで、使われるようになったのが Displacement Map です。

Displacement Map を使うと本当にポリゴンの表面を変異させてデコボコを作れます。GPU でシェーダーが使えるようになってから普及した手法ですね。

さて、問題は JavaFX の Displacement Map です。

DisplacementMap クラスはパッケージから分かるように Effect の一種です。使いこなせればけっこう強力なエフェクトですが、自分もまだ完全に使いこなしているわけではないのです...

まぁ、とりあえず分かっているところまでをやってみましょう。

Displacement Map はある座標を与えると、それを新しい座標に変換して描画します。2 次元で考えた場合、元の座標が (x, y) で新しい座標が (x', y') だったとします。この場合、座標の変換は次の式で与えられます。

  x' = x - (offsetX + scaleX * map(x, y)[0] * width)
  y' = y - (offsetY + scaleY * map(x, y)[1] * height) 

ここで、map(x, y) は座標を入力にした時に、変位を返す関数です。map(x, y)[0] が x 座標、map(x, y)[1] が y 座標だと考えてください。

offsetX, offsetY はその名の通りオフセットです。scaleX, scaleY は map 関数の倍率です。width と height は対象としているノードの幅と高さです。幅と高さをかけているということは、変位は相対値になります。

そして、map 関数を実現しているのが、FloatMap クラスです。

たとえば、イメージを x 軸方向に 10%、y 軸方向に 10% だけ平行移動をするには次のように記述します。

        FloatMap floatMap = new FloatMap(width, height);

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                floatMap.setSamples(x, y, -0.1f, -0.1f);
            }
        }

        DisplacementMap displacementMap = new DisplacementMap(floatMap);

        ImageView view = new ImageView(new Image(getClass().getResource("cat.jpg").toString()));
        view.setEffect(displacementMap);

DisplacementMap クラスのコンストラクタには FloatMap オブジェクトを指定しています。FloatMap オブジェクトだけを指定した場合、offset は 0、scale は 1 になります。

コンストラクタオーバーロードには offset や scale を指定できるものがありますが、ここでは FloatMap クラスの使い方をメインにするため、offset を 0、scale を 1 に限定します。

で、FloatMap クラスはコンストラクタで幅と高さを指定します。そして、setSample メソッドでサンプルを設定します。つまり、map(x, y) の結果を指定していることになります。

第 1 引数、第 2 引数が座標、第 3 引数、第 4 引数がその時の変位となります。FlotMap なので、変位を示す型は float です。

setSample メソッドなんて使わずに、map 関数をラムダ式で指定できるようになると使うのも簡単になると思うのですが、残念ながら対応しておらず。自分で作ってしまえと思ったのですが、FloatMap クラスと DisplacementMap クラスのソースを読み切れてないので、こちらもできてません ><

それはそれとして、ここでは 10% ずつ動かすので、-0.1f を指定しています。

では、実行してみましょう。

元の画像がこちらです。

これに対し、上記の DisplacementMap を施したものがこちら。

ここで、わかりやすさのために、もともとの画像の位置に赤枠を描画しています。右下にずれていることが分かりますよね。

たとえば、x 軸方向だけ 20% だけ拡大するならば、こんな感じ。

        FloatMap floatMap = new FloatMap(width, height);

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                floatMap.setSamples(x, y, -(float)x/width * 0.2f, 0.0f);
            }
        }

        DisplacementMap displacementMap = new DisplacementMap(floatMap);

        ImageView view = new ImageView(new Image(getClass().getResource("cat.jpg").toString()));
        view.setEffect(displacementMap);

で、実行してみたのがこちら。ずいぶん、丸顔になってしまいましたw


レンズ効果

簡単な使い方が分かったので、ちょっとした応用をしてみましょう。

一時期、イヌの鼻デカ写真が流行ったのを覚えているでしょうか。広角レンズや魚眼レンズで撮ったイヌの写真で、中央に鼻を持ってくると、鼻が強調して写るのです。

で、それをやってみたいわけです。

変位で考えると、中央は移動させず、中央の周りは変位を大きくして拡大、端っこになると縮小させれば、できるはずです。

しかも、中央の右側と左側、上と下で変位の符号を変えなくてはなりません。これを簡単にやるには... だいたいこういうのは三角関数というのが定番なんですよね。

中央で変位の符号を変えるには、sin で考えると、中央を 180 度 (ラジアンだとπ) にしてやれば OK です。x 方向で考えた時に、x = 0 と x = width の時に変位を 0 にするには、width で 1 周期、つまり 2π にすればいいわけです。

で、できた FloatMap オブジェクトはこんなのです。

        FloatMap floatMap = new FloatMap(width, height);

        for (int x = 0; x < width; x++) {
            float u = (float) Math.sin(Math.PI * x / width * 2.0) * 0.05f;
            for (int y = 0; y < height; y++) {
                float v = (float) Math.sin(Math.PI * y / height * 2.0) * 0.05f;
                floatMap.setSamples(x, y, u, v);
            }
        }

x 軸方向の変位が u、y 軸方向の変位が v です。最後に 0.05 を掛けているのが、最大の拡大率というわけです。つまり、最大 5% の拡大になります。

で、やってみました。

確かに中央部は拡大されているものの、ちょっと違う ><

まぁ、理由は分かっているんですけどね。本来はノードの中心からの極座標で変位を決めなくてはいけないのですが、手抜きしたのでこうなってしまっているわけです。

もうちょっと時間がある時に、再チャレンジしてみたいと思います。


Lens Effect

黒地に黄色のクローラー

この記事は、JavaFX Advent Calendar 2015 の 17 日目の記事です。

昨日は id:c9katayama さんの PDFBoxとFXGraphics2Dを使って大きなPDFをレンダリングする でした。

明日は @yumix_h さんです

タイトルの黒地に黄色のクローラーといえば、あれですよ、あれ。

明日の公開のあの映画。そう Star Wars です。こんなやつです。

http://img.lum.dolimg.com/v1/images/episode-3-crawl_4d843f60.jpeg

過去に、プレゼンのはじめに Star Wars のパロディのムービーを作ったことがある櫻庭ですから、この波に乗らないわけにはいきません。

ということで、JavaFX であのクローラーを作ってみようというわけです。

3D でやってみる

あのクローラーは文字列が奥のほうに移動しているので、まずは単純に 3D の API を使って、z 軸方向に移動させてみます。

でも、単に文字列を移動させても、台形の形にはならないので、文字列を x 軸を回転軸にして回転させます。ようするに、文字列を寝かせるわけですね。そして、z 軸方向に移動させるわけです。

まず、3D にするにはカメラを設定しなくてはいけません。

通常、2D の GUI では平行透視法のカメラが設定されています。このため、奥にあるノードでも小さく表示されることはありません。でも、3D の場合は、奥にあるノードは小さく表示しなくてはなりません。そのためには、それようのカメラ、つまり透視法を使用したカメラを設定します。

これは簡単で、Scene に PerspectiveCamera を設定します。

        Scene scene = new Scene(root);
        scene.setCamera(new PerspectiveCamera());

これで、奥の方のノードは小さく表示されるようになりました。

さて、次は文字列を寝かせる処理です。これはノードの setRotate メソッドで行います。しかし、単に setRotate メソッドをコールすると、z 軸を中心に回転してしまいます。これを x 軸を中心にさせるには setRoateAxis メソッドを使用します。

        Text text = new Text(crawl);

        text.setRotationAxis(new Point3D(1.0, 0.0, 0.0));
        text.setRotate(-80.0);

2 行目で、x 軸を表す Point3D オブジェクトを生成して、setRotationAxis メソッドに指定しています。そして、3 行目で後ろに 80 度倒す処理をします。JavaFX では角度はラジアンではなくてディグリーを使用することに注意してください。

準備ができたので、z 軸方向に移動するアニメーションを作成してみましょう。z 軸方向に移動する場合でも、移動は移動なので、TransTranslateTransition を使用します。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setToZ(1_000);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

プログラムの全体はブログの最後の方にまとめて載せました。

では、実行してみましょう。

確かに、奥に移動する感じはできましたけど、なんかちょっと違う...

奥の方に移動すると、x 軸方向に回転させた角度が変化してしまうのが気になります。3D なので、当たり前といえば当たり前なのですが。

また、いくら z 軸上で奥に移動させても、見えなくなるほどにはなりません。そのため、さいごの方の挙動が止まっているように見えてしまいます。

単純に 3D で移動させるよりも、もうちょっといい方法があるような気がします。もうちょっと模索してみましょう。

3D と 2D のアニメーションを組み合わせる

次に思いついたのが、3D の表現と 2D のアニメーションを組み合わせる方法です。

文字列を後ろに寝かせるのは 3D で行うとして、奥に移動するアニメーションは疑似的に 2D で表せないかということです。具体的には y 軸方向の上に移動するアニメーションと、縮小するアニメーションを組み合わすということです。

y 軸方向に移動させるアニメーションは先ほどと同じように TranslateTransition を使用します。また、拡大縮小には ScaleTransition を使用します。

まずは、移動のアニメーションです。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(1_000);
        trans.setToY(-100);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

単に y 軸上を動かしているだけです。

そして、拡大縮小です。はじめは拡大しておいて、徐々に縮小します。ただし、文字列を寝かしているため、z 軸方向にも大きさがあります。そのため、z 軸方向も拡大縮小します。

        ScaleTransition scale = new ScaleTransition(Duration.millis(20_000), text);
        scale.setFromX(5.0);
        scale.setFromY(5.0);
        scale.setFromZ(5.0);
        scale.setToX(0.01);
        scale.setToY(0.01);
        scale.setToZ(0.01);
        scale.setInterpolator(Interpolator.LINEAR);
        scale.setCycleCount(Animation.INDEFINITE);
        scale.play();

なお、複数のアニメーションを同時にやるためには ParallelTransition を使うこともありますが、そんなにきっちり合わす必要がなければ、単に複数のアニメーションを play してあげれば大丈夫です。

さて、これで実行してみましょう。

先ほどよりはいい感じになっていると思うのですが、どうでしょう。

しかし、これもアニメーションで移動させていると、文字列の寝ている角度が変わってしまうのが気になります。まぁ、こちらも、当たり前といったら当たり前ですが。

そこで、移動と同時に角度を変化させるアニメーションも一緒に行ってみます。

角度を変化させるアニメーションは RotateTransition です。

        RotateTransition rotate = new RotateTransition(Duration.millis(20_000), text);
        rotate.setToAngle(-80.0);
        rotate.setInterpolator(Interpolator.EASE_OUT);
        rotate.setCycleCount(Animation.INDEFINITE);
        rotate.play();

角度を変化させているといっても、ほんのちょっとです。それでも、見た目はかなり変化します。

ここでは回転の中心を設定していませんが、それははじめに文字列に対して設定しているからです。

これで同時に行うアニメーションは 3 つになりましたが、効果はどうでしょう。さっそく実行してみましょう。

かなりよくなったと思いませんか。これはこれでいいかもしれませんが、他の方法も模索してみましょう。

すべて 2D のアニメーションで行う

さいごに、3D はあきらめて、すべて 2D で行ってみましょう。もちろん、Star Wars の Episode IV の頃には CG はなかったのですから、2D のアニメーションでやっているはずです。

ということは、2D のアニメーションで実現するということは、原点回帰になるのかな?

もちろん、2D なので、Scene にカメラを設定させる必要もありません。

しかし、問題になるのは文字列を寝かせることができるかどうかです。実をいうと、これはエフェクトを使うことで、比較的簡単にできます。

どんなエフェクトかというと PerspectiveTransform です。先ほどまで使用していた PerspectiveCamera と同じく、透視法が使えるエフェクトというわけです。

PerspectiveTransform はノードを囲む四隅を任意の点に写像することができるエフェクトです。AffineTransform が実現するアフィン変換だと平行は平行に変換されますが、PerspectvieTransform では平行は維持されません。つまり長方形を台形などに変換することができるのです。

変換には四隅の座標を指定します。

        PerspectiveTransform perspective = new PerspectiveTransform();
        perspective.setUlx(300.0);
        perspective.setUly(200.0);

        perspective.setUrx(500.0);
        perspective.setUry(200.0);

        perspective.setLlx(0.0);
        perspective.setLly(500.0);

        perspective.setLrx(800.0);
        perspective.setLry(500.0);

        text.setEffect(perspective);

それぞれ 2 行ずつで、上左、上右、下左、下右の座標を指定しています。ここでは、ルートのコンテナの座標を台形にするような変形になります。

そして、後は先ほどと同じように y 軸上を移動するアニメーションと拡大縮小を組み合わせます。ただし、拡大縮小はノードの真ん中を中心に拡大縮小してしまうため、上記の変換と合わせると変な位置に変換されてしまいます。

そこで、Scale を使用して、拡大縮小の中心を変化させています。Scale を使ってアニメーションをするため、Transition ではなく Timeline を使用しています。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(400);
        trans.setToY(0);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

        Scale scale = Transform.scale(4.0, 4.0, 400.0, 200.0);
        text.getTransforms().add(scale);
        Timeline timeline = new Timeline(
                new KeyFrame(Duration.millis(19_000),
                        new KeyValue(scale.xProperty(), 0.3), 
                        new KeyValue(scale.yProperty(), 0.3)),
                new KeyFrame(Duration.millis(20_000),
                        new KeyValue(scale.xProperty(), 0.001), 
                        new KeyValue(scale.yProperty(), 0.001))
        );
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();

拡大縮小のアニメーションを 2 段階にしているのは、よりそれっぽさを出すためです。

では、実行してみましょう。

PerspectiveTransform と Scale を組み合わせているせいか、アニメーション開始時の拡大している状態では少しノードの表示が荒れてしまっていますが、動きは一番それらしいのではないでしょうか。

ということで、3 種類の方法で Star Warsクローラーを実現してみました!

これで、時代の波に乗れたかなww

プログラム

3D でやってみる


Crawler Demo 1

3D と 2D のアニメーションを組み合わせる


Crawler Demo 2

すべて 2D のアニメーションで行う


Crawler Demo 3

JavaFX と Project Jigsaw

この記事は、JavaFX Advent Calendar 2015 の 12 日目の記事です。

昨日は @y_q1m さんの JavaFX アプリケーションに隠し Control を搭載する でした。

明日は @tomo_taka01 さんです

ごめんなさい。今日も小ネタです。

JavaOne に行ってからというもの、Project Jigsaw が面白くなっていろいろやってます。

ところで、JavaFXJava SE 9 ではあまり新機能が盛り込まれないようなのです。Feature Complete がちょっと伸びたので、もう少し盛り込むようですが...

で、なんで新機能が少ないかというと、Project Jigsaw への対応が大変だったからということらしいのです。JavaFX 9 については、 id:aoe-tk さんの JavaFX9に追加される機能が増えるかもしれません を見ていただくとして、「じゃあ JavaFX のモジュールはどうなったの?」というところをちょっと調べてみました。

Java SE 9 の java コマンドには -listmods というオプションが追加になって、標準のモジュールの一覧を表示することができます。まずは、これを試してみましょう。

C:\>java -listmods 2>&1 | grep javafx -
javafx.base@9.0
javafx.controls@9.0
javafx.deploy@9.0
javafx.fxml@9.0
javafx.graphics@9.0
javafx.media@9.0
javafx.swing@9.0
javafx.web@9.0

8 個のモジュールが JavaFX に関連するようです。

基本となるのが javafx.base で、コントロールは javafx.controls というようになっているのはわかるのですが、javafx.graphics などがよく分かりません。javafx.base が基本というのは分かりますけど、基本というのはどのぐらいの範囲、つまりどういうクラスが含まれているのかなどもよく分かりません。

これを調べるために、簡単なクラスで試してみました。

public class Test extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }
}

単にステージを表示するだけのクラスです。これが含まれる JAR ファイルが test.jar だとします。で、この JAR ファイル (or クラス) がどのようなモジュールを使用しているかを調べるには、jdeps コマンドを使用します。

jdeps -s test.jar
test.jar -> java.base
test.jar -> javafx.graphics

オプションの -s はサマリだけを出力させるオプションです。

ステージを表示するだけで、他には何もやっていないのだから javafx.base に依存しているだろうと思ったら、javafx.graphics に依存していました。もちろん、javafx.graphics は javafx.base に依存しているでしょうから、間接的には依存はしているのですが...

-s オプションを外すと、もうちょっと詳しく見ることができます。

jdeps test.jar
test.jar -> java.base
test.jar -> javafx.graphics
   <unnamed> (test.jar)
      -> java.lang
      -> javafx.application
      -> javafx.stage

やはり、Application クラスや Stage クラスは javafx.graphics モジュールに含まれるようです。

ちょっとサンプルを改造してみましょう。

public class Test extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Label label = new Label("Label");
        
        StackPane root = new StackPane(label);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }
}

こうすると、依存しているモジュールには javafx.control が出てくるはず。

jdeps -s test.jar
test.jar -> java.base
test.jar -> javafx.controls
test.jar -> javafx.graphics

やはり、javafx.controls に依存していました。

しかし、javafx.base や javafx.graphics はよく分からないままです。こういう時は、モジュールを直接調べてみましょう。

一般的にモジュールには JAR ファイルを使用するのですが、標準のモジュールは JAR ファイルではなく、JMOD ファイルになっています。モジュールが JAR ファイルの時は jar コマンドで調べられるのですが、JMOD ファイルの時は jmod コマンドを使用します。どちらもオプションとして -p (--print-module-descriptor) を使用すれば、モジュールの情報を表示してくれます。

さっそくやってみましょう。JMOD ファイルは JDK のディレクトリの jmods ディレクトリにあります。

C:\Program Files\Java\jdk1.9.0\jmods>jmod javafx.base.jmod -p

Name:
  javafx.base@9.0
Requires:
  jdk.jfr
  java.base [ MANDATED ]
  java.desktop
Exports:
  com.sun.javafx to [javafx.controls, javafx.graphics, javafx.swing]
  com.sun.javafx.beans to [javafx.controls, javafx.graphics, javafx.fxml]
  com.sun.javafx.collections to [javafx.controls, javafx.graphics, javafx.swing, javafx.media]
  com.sun.javafx.binding to [javafx.controls, javafx.graphics]
  com.sun.javafx.event to [javafx.controls, javafx.graphics]
  com.sun.javafx.logging to [javafx.graphics]
  com.sun.javafx.property to [javafx.controls]
  com.sun.javafx.runtime to [javafx.graphics]
  javafx.beans
  javafx.beans.binding
  javafx.beans.property
  javafx.beans.property.adapter
  javafx.beans.value
  javafx.collections
  javafx.collections.transformation
  javafx.event
  javafx.util
  javafx.util.converter
Conceals:
  com.sun.javafx.property.adapter
Hashes:
  Algorithm: SHA-256
  java.desktop: AE4yDbzkIjSeteMHRe9ZMpEkjE/u4PMGSL7WCwpEyOw=
  jdk.jfr: 6hhBHpCTp8ZkLutorg8GIFCp+D9MrL++mIsTh6grzyE=
  java.base: Jjip1psj45/C01FcGzXZc83cwqFHAZVVL3wSXJLiMpc=

外に公開しているパッケージは Exports の項に書いてあります。実をいうと、Exports の部分は順番がぐちゃぐちゃだったので、ソートしなおしてあります。

これを見ると、javafx.base は、意外にもグラフィックに関する部分はまったく含んでおらず、プロパティやバインドに関するクラスだけを含んでいるようです。

ただし、Requires の項に、java.desktop が含まれていることに注意が必要です。java.desktop は AWT や Swing が含まれているモジュールです。これに依存しているので、まったくグラフィックに関連がないというわけではないことが分かります。

次に、javafx.graphics です。

C:\Program Files\Java\jdk1.9.0\jmods>jmod javafx.graphics.jmod -p

Name:
  javafx.graphics@9.0
Requires:
  java.base [ MANDATED ]
  java.desktop
  java.xml
  javafx.base [ PUBLIC ]
Exports:
  com.sun.glass.ui to [javafx.web, javafx.media]
  com.sun.glass.utils to [javafx.web, javafx.media]
  com.sun.javafx.application to [javafx.web, javafx.controls, javafx.deploy, javafx.swing]
  com.sun.javafx.cursor to [javafx.deploy, javafx.swing]
  com.sun.javafx.css to [javafx.controls, javafx.deploy]
  com.sun.javafx.css.converters to [javafx.deploy]
  com.sun.javafx.css.parser to [javafx.deploy]
  com.sun.javafx.embed to [javafx.deploy, javafx.swing]
  com.sun.javafx.font to [javafx.web]
  com.sun.javafx.geom to [javafx.web, javafx.controls, javafx.swing, javafx.media]
  com.sun.javafx.geom.transform to [javafx.web, javafx.controls, javafx.swing, javafx.media]
  com.sun.javafx.iio to [javafx.web]
  com.sun.javafx.jmx to [javafx.web, javafx.swing, javafx.media]
  com.sun.javafx.menu to [javafx.controls, javafx.deploy]
  com.sun.javafx.perf to [javafx.deploy]
  com.sun.javafx.scene to [javafx.web, javafx.controls, javafx.deploy, javafx.swing, javafx.media]
  com.sun.javafx.scene.input to [javafx.web, javafx.controls, javafx.swing]
  com.sun.javafx.scene.text to [javafx.web, javafx.controls]
  com.sun.javafx.scene.traversal to [javafx.web, javafx.controls]
  com.sun.javafx.sg.prism to [javafx.web, javafx.swing, javafx.media]
  com.sun.javafx.stage to [javafx.controls, javafx.deploy, javafx.swing]
  com.sun.javafx.text to [javafx.web, javafx.deploy]
  com.sun.javafx.tk to [javafx.web, javafx.controls, javafx.deploy, javafx.swing, javafx.media]
  com.sun.javafx.util to [javafx.web, javafx.controls, javafx.fxml]
  com.sun.prism to [javafx.web, javafx.media]
  com.sun.prism.image to [javafx.web]
  com.sun.prism.paint to [javafx.web]
  com.sun.scenario.effect to [javafx.web]
  com.sun.scenario.effect.impl to [javafx.web]
  com.sun.scenario.effect.impl.prism to [javafx.web]
  javafx.animation
  javafx.application
  javafx.concurrent
  javafx.css
  javafx.css.converter
  javafx.geometry
  javafx.print
  javafx.scene
  javafx.scene.canvas
  javafx.scene.effect
  javafx.scene.image
  javafx.scene.input
  javafx.scene.layout
  javafx.scene.paint
  javafx.scene.shape
  javafx.scene.text
  javafx.scene.transform
  javafx.stage
Conceals:
   <<省略>>
Hashes:
  Algorithm: SHA-256
  javafx.base: vKYsvlQtkuWc5pwKzHKlUfIp9lXa4SAjnU1TniODA74=
  java.xml: 1FV2E7DCyzOAroiUpxIyJMSg+AzeDI5qm1SmeFyqv4U=
  java.desktop: AE4yDbzkIjSeteMHRe9ZMpEkjE/u4PMGSL7WCwpEyOw=
  java.base: Jjip1psj45/C01FcGzXZc83cwqFHAZVVL3wSXJLiMpc=

こちらも、ソートしなおしてあります。

Applicationや、Scene、Stage も javafx.graphics に含まれていることが分かります。また、イベント関連、CSS 関連なども含まれています。

ちょっと意外だったのが、Animation や Shape も含まれていることです。こういうクラスたちは、外だしされるかと思ってましたけど、基本のところに含まれるんですね。

私がいつも使っているクラスたちは、ほぼ javafx.graphics で済んでしまうということも分かりました。あんまり、コントロール使わないんですよね ^ ^;;; もちろん、仕事では使ってますけど。

ということで、今日はここまで。