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