torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFX 3DのSphareに貼るテクスチャのマッピング(続々)

JavaFX 3DのSphareに貼るテクスチャのマッピング(続) - torutkのブログ
の続きです。

前回のあらすじ

前回は、JavaFX 3Dの球面へ貼り付けたテクスチャ画像の表示が縦方向に潰れた様相になっていたため、あらかじめテクスチャ画像を正積円筒図法の地図投影変換をかけておくことでそれらしい表示を実現することができることを示しました。

球面のuvマッピング

3次元直交座標系XYZの球面に、テクスチャマップを貼る場合のマッピング(uvマッピング)は、
https://en.wikipedia.org/wiki/UV_mapping
によれば次の式となります。

u = 0.5 + arctan2(dz, dx) / 2Ï€
v = 0.5 - arcsin(dy) / π

(dx, dy, dz)は、球面上の点Pにおける原点への単位ベクトル

なお、(u, v)はテクスチャとなる画像の左上を原点とし、幅をu、縦をvとした座標です。
XYZ軸のXY軸は画面の横・縦方向、Z軸は画面の奥行方向となります。

JavaFXにおける球面のuvマッピング

JDK 8のJavaFXではどのように球面のuvマッピングを実装しているかを調べてみます。
JDK 8に添付されるソースファイル(javafx-src.zip)に、JavaFXのAPIのソースが含まれています。
この中にあるjavafx.scene.shape.Sphereクラスのメソッド createMesh にuvマッピングの処理が記載されているようです。

static TriangleMesh createMesh(int div, float r) {
   :
}

div は、Sphereのコンストラクタの第2引数で分割数(球をポリゴンで構成したときのポリゴン数)です。
通常省略するのでデフォルトの64が適用されます。
r は、Sphereの半径です。

このメソッドの中でuvマッピング計算をしている主要な箇所は次となります。

       for (int y = 0; y < div2 - 1; ++y) {
            float va = rDiv * (y + 1 - div2 / 2) * 2 * (float) Math.PI;
            float sin_va = (float) Math.sin(va);
            float cos_va = (float) Math.cos(va);

            float ty = 0.5f + sin_va * 0.5f;
            for (int i = 0; i < div; ++i) {
                double a = rDiv * i * 2 * (float) Math.PI;
                float hSin = (float) Math.sin(a);
                float hCos = (float) Math.cos(a);
                points[pPos + 0] = hSin * cos_va * r;
                points[pPos + 2] = hCos * cos_va * r;
                points[pPos + 1] = sin_va * r;
                tPoints[tPos + 0] = 1 - rDiv * i;
                tPoints[tPos + 1] = ty;                     <-- ここが問題
                pPos += 3;
                tPos += 2;
            }
            tPoints[tPos + 0] = 0;
            tPoints[tPos + 1] = ty;
            tPos += 2;
        }

vaは、球面上の点を極座標で表したときの垂直方向の角度で(-2/π,2/π]を取ります。
aは、球面上の点を極座標で表したときの水平方向の角度で、(0, 2π]を取ります。
pointsは球面上の点の座標(x, y, z)を格納し、tPointsはテクスチャ画像上の点の座標(u, v)を格納すると思われます。
ここで、

tPoints[tPos + 1] = ty;

とあるのが問題箇所ではないかと仮定します。tyは

float ty = 0.5f + sin_va * 0.5f;

となっており、球面上の点を極座標で表したときの垂直方向の角度を、sin関数でテクスチャ画像上の点の座標に変換しています。
しかし、前述のWikipediaのUVマッピングの式であれば、本来は、0.5 - arcsin(-sin(va)) / π となるのが正かと思われます。

そこで、javafx.scene.shape.Sphereのソースファイル(Sphere.java)を

float ty = 0.5f + (float) (Math.asin(sin_va) / Math.PI);

と書き直してみました。このShere.javaをコンパイルし、生成されたSphere.classを、
JDKインストールディレクトリのjre/lib/ext/jfxrt.jar の中のSphere.classと差し替えます。

差し替え前後の表示を見てみます。

まず使用するテクスチャ画像は次です。

UVマッピング修正前 UVマッピング修正後

ということで、差し替え後は他の多くの3Dグラフィックライブラリにおける球面のテクスチャと同じ表現となりました。

この先は

一般的なオープンソースならバグ報告からなので、Javaのバグデータベースのサイトを開き、
http://bugs.java.com/

"Sphere texture"で検索してみたところ、まさにこの問題が存在していました。
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8092112

が、コメントを見る限り、挙動は修正せず、今後enumを用意してテクスチャマッピング方式を選択可能とするかも(今は実装されていない)といったことのようです。