ControlsFX 準備編 その2

このEntryは JavaFX Advent Calendar の最終日のエントリーです。

昨日は id:yumix_h さんの JavaFXのHTMLEditorの機能と限界 でした。

今日もControlsFXの紹介です。

23日のエントリー でモジュールではないアプリケーションからの使い方を紹介しました。

今日はモジュールアプリケーションからControlsFXを使用します。

でも、その前にProject Jigsawのモジュールについて簡単に解説しましょう。ここでは、JavaFXでの例で示していますけど、他のモジュールアプリケーションでも同じようなことはあるはずです。

モジュール

module-info.javaがあればモジュールです。そして、module-info.javaには依存性(requires文)、公開範囲(exports文)を記述します。

基本的にはこれだけですが、FXMLを使用している場合、コントローラクラスがリフレクションでアクセスされるため、それを許可するopens文も欠かせません。

では、前回のサンプルがどのようなモジュールに依存しているか調べてみましょう。そのためには、jdepsコマンドを使用します。

C:\controlsfx\controlsfxdemo>jdeps -cp lib\controlsfx-9.0.0.jar -s dist\controlsfxdemo.jar
controlsfxdemo.jar -> lib\controlsfx-9.0.0.jar
controlsfxdemo.jar -> java.base
controlsfxdemo.jar -> javafx.fxml
controlsfxdemo.jar -> javafx.graphics

java.base、javafx.fxml、javafx.graphicsの3つのモジュールに依存していることが分かります。ただし、java.baseだけはmodule-info.javaに記述する必要はありません。

公開するのは、net.javainthebox.fxパッケージなので、これを含めてmodule-info.javaを記述します。

ここでは、モジュール名をnet.javainthebox.controlsfxdemoとします。

module net.javainthebox.controlsfxdemo {
    requires javafx.fxml;
    requires javafx.graphics;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

しかし、これだけではビルド、実行させることができません。もちろん、ControlsFXの扱いのためです。

controlsfx-9.0.0.jarはモジュールではないので、注意する必要があります。

モジュールの種類

実をいうと、モジュールには3種類あります。

module-info.javaが存在するのが普通のモジュールですが、それ以外に2種類のモジュールがあります。

  • 通常のモジュール
  • 自動モジュール (Automatic Module)
  • 名前なしモジュール (Unnamded Module)

Automatic ModuleもUnnamed Moduleもmodule-info.javaがないモジュールです。これらの違いはモジュールが直接依存しているのか、間接的に依存しているかという違いです。

  • モジュールが直接アクセスするJARファイル -> Automatic Module
  • モジュールがアクセスするJARファイルがアクセスするJARファイル -> Unnamed Module

ということになります。

moudle-info.javaを含むモジュールはクラスパスを使用することができず、すべてモジュールパスで扱います。モジュールでないJARファイルもモジュールパスで指定したディレクトリにおけば、モジュールとして扱われるのです。

そして、そのモジュールとして扱われるJARファイルをAutomatic Moduleと呼びます。

モジュールパスでアクセスするということは、module-info.javaに記述しなくてはいけません。そのため、Automatic Moduleとして扱われるJARファイルはモジュール名が必要になります。

この場合、マニフェストファイルMANIFEST.MFのAutomatic-Module-Name属性で指定された名前を使用します。Automatic-Module-Name属性が指定されていない場合、JARファイルの名前がモジュール名として使用されます(バージョン表記などが含まれていても、それは取り除かれます)。

controlsfx-9.0.0.jarファイル場合、Automatic-Module-Name属性は記述されていません。そのため、ファイル名からcontrolsfxがモジュール名として使用されます。

さらに、Automatic Moduleが使用するJARファイルがUnnamed Moduleとして扱われます。これは名前がないので、今までと同じようにクラスパスで指定します。

では、module-info.javaにAutomatic Moduleのcontrolsfxモジュールを追加してみます。

module net.javainthebox.controlsfxdemo {
    requires javafx.fxml;
    requires javafx.graphics;

    requires controlsfx;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

これで、ビルドはできるようになります。しかし、実行してみると...

C:\controlsfx\controlsfxdemo>java -p dist;lib -m net.javainthebox.controlsfxdemo/net.javainthebox.fx.ControlsFXDemo
Exception in Application start method
java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(Unknown Source)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(Unknown Source)
Caused by: java.lang.RuntimeException: Exception in Application start method
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NoClassDefFoundError: javafx/scene/control/TextField
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
        at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
        at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadTypeForPackage(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadType(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.importClass(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.processImport(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.processProcessingInstruction(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.load(Unknown Source)
        at net.javainthebox.controlsfxdemo/net.javainthebox.fx.ControlsFXDemo.start(ControlsFXDemo.java:14)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(Unknown Source)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(Unknown Source)
        ... 1 more
Caused by: java.lang.ClassNotFoundException: javafx.scene.control.TextField
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        ... 33 more
Exception running application net.javainthebox.fx.ControlsFXDemo

C:\controlsfx\controlsfxdemo>

javafx.scene.control.TextFieldクラスが見つからないという例外が起こってしまいました。

これはcontrolsfxがJavaFXのモジュールに依存しているためです。しかし、Automatic Moduleでは依存性が記述できないため、TextFieldクラスを含むモジュールをロードできないためです。

その場合、controlsfxが依存するモジュールも一緒にmodule-info.javaに記述します(起動時オプションで指定する方法もあります)。

controlsfxが依存しているモジュールを調べるために、jdepsコマンドを使用してみましょう。

C:\controlsfx\controlsfxdemo>jdeps -s lib\controlsfx-9.0.0.jar
controlsfx-9.0.0.jar -> java.base
controlsfx-9.0.0.jar -> java.desktop
controlsfx-9.0.0.jar -> javafx.base
controlsfx-9.0.0.jar -> javafx.controls
controlsfx-9.0.0.jar -> javafx.graphics
controlsfx-9.0.0.jar -> javafx.media
controlsfx-9.0.0.jar -> javafx.web

ここで表示されたすべてのモジュールを記述する必要はありません。実際に使用しているモジュール、ここではTextFieldクラスが含まれるjavafx.controlsモジュールだけを追加すればOKです。

module net.javainthebox.controlsfxdemo {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.graphics;

    requires controlsfx;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

これでモジュールのアプリケーションからControlsFXを使用することができます。

なお、NetBeansではビルドはできるものの、実行はエラーになってしまいます。いろいろ試してみたのですが、結局実行できませんでした。

なので、実行だけはコマンドラインから行っています。

準備編だけで長くなってしまったので、個々のコントロールについてはまた後日。

ControlsFX 準備編

このEntryは JavaFX Advent Calendar の22日目のエントリーです。

昨日は id:hagi44 さんの 業務で JavaFX をちょっとだけ使ってみた でした。明日は id:yumix_h さんが再び登場です。

なんか、ITproの連載が終わってから、完全にJavaはオフモードになってしまっていますが、久しぶりにblog書くのではてな記法もぜんぜん覚えていないというていたらく。

まぁ、仕事ではJavaJavaFXも使っているので、Javaに触っていないというわけではないのですが...

ということで、今日はControlsFXを紹介しようと思います。

ControlsFX

ControlsFXはJonathan Gilesさんを中心にJavaFXのコントロールを提供するためのプロジェクトです。

ControlsFXのコントロールの中には、DialogのようにJavaFXの標準コントロールに採用されたものもあります。

今年のJavaOneのDuke's Choice Awardも獲得しています!

とことが、JonathanがOracleからMicrosoftに転職してしまったのです!

ということで、Duke's Choice Award受賞記念と、転職のはなむけにControlsFXを紹介していきます!!

ControlsFXが提供しているコントロールおよびその周辺機能としては、以下のようなものがあります。

  • Actions
  • Borders
  • BreadcrumbBar
  • ButtonBar
  • CheckComboBox / CheckListView / CheckTreeView
  • Decoration / Validation
  • Dialogs
  • FXSampler
  • Glyph font pack support
  • GridView
  • HiddenSidesPane
  • HyperlinkLabel
  • InfoOverlay
  • ListSelectionView
  • MasterDetailPane
  • Notifications / NotificationPane
  • PlusMinusSlider
  • PopOver
  • PropertySheet
  • RangeSlider
  • Rating
  • SegmentedButton
  • SnapshotView
  • SpreadsheetView
  • TaskProgressView
  • TextFields
  • Top Quality JavaDocs!
  • Translations

このリストはContorlsFXのサイトからコピペしてきたものなので、私もよくわかっていないものも入ってます ^ ^;;

今日は準備編として、ControlsFXを使えるところまで紹介します。

ダウンロードとサンプルの実行

ControlsFXは現状、2つのバージョンが公開されています。

  • Java SE 9用のControlsFX 9.0.0
  • Java SE 8用のControlsFX 8.40.14

当然、Java SE 9用のControlsFX 9.0.0を使っていきます。

ダウンロードは ダウンロード用のリンク から行います。

ダウンロードするファイルはcontrolsfx-9.0.0.zipです。このファイルを展開すると、ライセンスのテキストとJARファイルが3つあります。

この中のcontrolsfx-samples-9.0.0.jarファイルがサンプルです。さっそく実行してみましょう。

C:\controlsfx-9.0.0>java -jar controlsfx-samples-9.0.0.jar
Initialising FXSampler sample scanner...
        Discovering projects...
                Found project 'ControlsFX', with sample base package 'org.controlsfx.samples'
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by impl.org.controlsfx.ReflectionUtils (file:/C:/controlsfx-9.0.0/controlsfx-9.0.0.jar) to method com.sun.javafx.css.StyleManager.getInstance()
WARNING: Please consider reporting this to the maintainers of impl.org.controlsfx.ReflectionUtils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

実行はできましたけど、なんか怒られてます。

これはJava SE 9のモジュール化の影響で、隠蔽しているクラスをリフレクションで使用しているために警告されているのです。メッセージにあるように--illegal-access=warnを実行時オプションとして付記すれば、警告はでますが、問題なく実行できます。

逆に--illegal-access=warnを付けないと、実行できたとしても、リフレクションが正常に動作しない部分があるはずです(確かめてないですけど)。

では、--illegal-access=warnを付けて、実行してみましょう。

C:\controlsfx-9.0.0>java --illegal-access=warn -jar controlsfx-samples-9.0.0.jar
Initialising FXSampler sample scanner...
        Discovering projects...
                Found project 'ControlsFX', with sample base package 'org.controlsfx.samples'
WARNING: Illegal reflective access by impl.org.controlsfx.ReflectionUtils (file:/C:/controlsfx-9.0.0/controlsfx-9.0.0.jar) to method com.sun.javafx.css.StyleManager.getInstance()
WARNING: Illegal reflective access by impl.org.controlsfx.ReflectionUtils (file:/C:/controlsfx-9.0.0/controlsfx-9.0.0.jar) to method com.sun.javafx.css.StyleManager.addUserAgentStylesheet(java.lang.String)

先ほどとはメッセージが変わっています。警告はしたけれども、実行できているということです。

実行すると、以下のようなウィンドウが表示されます。

右側の項目を選択すれば、その機能のデモが中央に表示されます。そして、右側に説明が表示されます。

たとえば、Bordersを選択すると、こんな感じ。

こんな感じで動かしてみれば、どういうコントロールが提供されているか分かるはずです。

Scene Builderへの組み込み

コントロールを使うのであれば、Scene Builderで使えなければ!!

ということで、Scene Builderでも使えるようにしてみましょう。

Scene Builderの左側のコントロールのリストの上に歯車アイコンがあります。

そこをクリックすると、下のようなメニューが表示されます。

で、JAR/FXML Managerを選択します。すると、Library Managerダイアログが表示されます。

ローカルにあるJARファイルを追加する場合は3番目の[Add Library/FXML from file system]を選択します。すると、ファイルチューザーが表示されるので、ダウンロードしたcontrolsfx-9.0.0.jarを指定します。

すると、JARファイルに含まれているコントロールが一覧表示されます。

必要なコントロールをチェックして (というか、全部チェックすればOKですが)、[Import Components]を選択すれば、OK。

すると、コントロールの一覧に[Custom]グループが追加されて、そこに今追加したコントロールが表示されます。

Mavenなどのレポジトリからダウンロードすることもできます。その場合は一番上の[Search repositories]もしくは、次の[Manually add Library from repository]を選択します。

[Manually add Library from repository]から説明しましょう。この項目を選択すると、Add Library from Repositoryダイアログが表示されます。

上の図のようにGroup IDにはorg.controlsfx、Artifact IDにはcontrolsfxを指定します。すると、バージョンが選択できるようになるので、9.0.0を選択します。

[Search repositories]の場合、先ほどのGroup IDもしくはArtifact IDをどちらかを入力すると、それに合致するライブラリを表示するので、追加します。

これで、標準のコントロールと同じようにControlsFXのコントロールを使用することができます。

試しに、AnchorPaneにCustomTextFieldを貼ってみたのが、下の図です。

このFXMLはこんな感じになってました。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<?import org.controlsfx.control.textfield.CustomTextField?>


<AnchorPane prefHeight="200.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <CustomTextField fx:id="textField" layoutX="74.0" layoutY="63.0" promptText="Custom TextField" AnchorPane.leftAnchor="50.0" AnchorPane.rightAnchor="50.0" AnchorPane.topAnchor="50.0">
         <font>
            <Font size="16.0" />
         </font>
      </CustomTextField>
   </children>
</AnchorPane>

ControlsFXを使用する (非モジュールアプリケーション)

といっても、普通のライブラリと同じです。

ここでは、NetBeansのNightly Buildを使います。

それにしても、NetBeansはいつになったら、Java SE 9に対応したバージョン出してくれるんでしょうねぇ...

試しに、controlsfxdemoというプロジェクトを作成しました。

Mavenのプロジェクトではなく、普通のプロジェクトなので、ライブラリにcontrolsfx-9.0.0.jarを追加してください。

では、先ほどのFXMLをそのまま使ってみます。

メインクラスをnet.javainthebox.fx.ControlsFXDemo、FXMLをControlsFXDemoView.fxml、コントローラクラスをnet.javainthebox.fx.ControlsFXDemoControllerとしました。

プロジェクトの構成はこんな感じ。

ControlsFXDemoクラスはFXMLをロードするだけです。

package net.javainthebox.fx;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class ControlsFXDemo extends Application {
    
    @Override
    public void start(Stage stage) throws IOException {
        AnchorPane root = FXMLLoader.load(getClass().getResource("ControlsFXDemoView.fxml"));
        
        Scene scene = new Scene(root);
        
        stage.setTitle("ControlsFX Demo");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

FXMLは先ほどのFXMLにコントローラクラスの指定を加えてます(整形してあります)。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<?import org.controlsfx.control.textfield.CustomTextField?>

<AnchorPane prefHeight="200.0" prefWidth="400.0" 
            xmlns="http://javafx.com/javafx/9.0.1" 
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="net.javainthebox.fx.ControlsFXDemoController">
   <children>
      <CustomTextField fx:id="textField" 
                       layoutX="74.0" layoutY="63.0" 
                       promptText="Custom TextField" 
                       AnchorPane.leftAnchor="50.0" AnchorPane.rightAnchor="50.0" AnchorPane.topAnchor="50.0">
         <font>
            <Font size="16.0" />
         </font>
      </CustomTextField>
   </children>
</AnchorPane>

ControlsFXDemoControllerクラスは、以下の通り。というか、何もやってないです。

package net.javainthebox.fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import org.controlsfx.control.textfield.CustomTextField;

public class ControlsFXDemoController implements Initializable {

    @FXML
    private CustomTextField textField;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }    
    
}

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

何も問題ないはず。

問題なのは、モジュールアプリケーションにした場合なのですが、それは次回。

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