今回はJavaFX上の3Dオブジェクトのマテリアル設定方法を見ていく。マテリアルとは、物体の質感を表現するもので、色やテクスチャなどが該当する。
■ 色と光に関する基礎知識
物体の色は『光』と『物体の性質』によって決定される。3Dグラフィックの世界では『物体の性質』をマテリアルと呼び、マテリアルに光が当たった場合の性質を設定することにより色が決定される。また、3Dグラフィックの世界で光を大別すると以下のようになる。
光は物体に当たることにより反射もしくは透過する。反射には2種類存在する。1つは『鏡面反射光』といい鏡のように完全に反射する光である。鏡面反射光の例としては、金属や水面での反射光がある。鏡面反射光は指向性で、見る角度によっては全く見えなかったりする。
もう1つの反射が『拡散反射光』といい、いわゆる物体の色である。拡散反射光は物体内部で特定の色の光を吸収し、それ以外の光を反射することにより色を作り出す。また拡散反射光は物体内部で何度も反射するため全方向に拡散される。このため、見る角度によらず一定の色に見える。
また、分かりにくい概念として環境光がある。例えば、赤い壁の近くにあるものは赤味がかって見えたりするが、これは赤い壁に反射した光の影響である。このように周りの物体からの反射・透過光を、まとめて環境光と呼ぶようである。
以上を表にまとめると以下のようになる。JavaFXでは、マテリアル設定でこれらの光がどのような見え方になるかを設定する。
光の種類 |
内容 |
発光(Emissive) |
自らが発する光 |
拡散(反射)光(Diffuse) |
物体内部に入り再度表面に現れる光。全方向性。
いわゆる物体の色と呼ばれるもの。 |
(鏡面)反射光(Specular) |
物体の表面で反射する光。指向性。
例)鏡(金属)や水面がキラキラときらめく光 |
透過光(Transmit) |
物体の内部を通過する光。
例)ガラス越しに見える光 |
環境光(Ambient) |
周りの物体の影響による光 |
■ JavaFXで設定可能なマテリアル
JavaFXで設定可能なマテリアルはPhongMaterialクラスのみであり、同クラスでは以下のマテリアル設定が可能である。『○○Map』というプロパティは画像を設定するプロパティである。
例えば、テクスチャの設定ではsetDiffuseMap関数を用いてdiffuseMapプロパティに画像を設定する。画像にはGIFアニメーションや透過GIF/PNGも設定可能である。テクスチャのアニメーションは『
JavaFX 3D メッシュ・テクスチャのアニメーション』を参照のこと。
分類 |
マテリアル名称 |
プロパティ |
内容 |
Diffuse |
色 |
diffuseColor |
物体の基本色 |
テクスチャ |
diffuseMap |
いわゆるテクスチャ。基本色のマップ |
Specular |
色 |
specularColor |
反射光の色 |
強度 |
specularPower |
反射の強度 |
スペキュラマップ |
specularMap |
反射の強度マップ |
Emissive |
自己照明 |
selfIlluminationMap |
発光の強度を表すマップ |
その他 |
バンプマップ |
bumpMap |
法線(面に垂直な線)の方向マップ |
バンプマップは物体の表面に凹凸があるように見せる技法であり、詳細な利用方法はサンプルにて確認する。
■ サンプルコード
以下にJavaFXにて、マテリアル設定を確認するサンプルコードを示す。サンプルでは上段4つ、下段4つの合計8つのオブジェクトを表示している。上段は球体に各種マテリアルを設定する例であり、下段は各種3Dオブジェクトにテクスチャ(DiffuseMap)を設定する例である。
◇サンプルコード
package application_fx;
import java.io.File;
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.LightBase;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Sphere;
import javafx.scene.shape.TriangleMesh;
import javafx.stage.Stage;
public class TestTexture extends Application {
public static void main(String[] args)
{ launch( args ); }
@Override
public void start(Stage primaryStage) throws Exception
{
// シーングラフの構成
Group root = new Group();
// テクスチャ画像を作成
String filePath = new File( "img/diffuseMap.jpg" ).toURI().toString();
Image diffuseImg = new Image( filePath );
// 各種マップの例(上段)
root.getChildren().add( createColorSphere() );
root.getChildren().add( createBumpMapSphere() );
root.getChildren().add( createSpecularMapSphere() );
root.getChildren().add( createSelfIlluminationMapSphere() );
// オブジェクト別のdiffuseMapの例(下段)
root.getChildren().add( createSphere( diffuseImg ) );
root.getChildren().add( createBox( diffuseImg ) );
root.getChildren().add( createCylinder( diffuseImg ) );
root.getChildren().add( createTriangleMesh( diffuseImg ) );
// シーンの作成
// 3Dシーンの奥行きを表現するため、Zバッファを有効にする
Scene scene = new Scene( root , 500 , 350 , true );
scene.setFill( Color.web( "9FCC7F" ) );
// カメラ設定
PerspectiveCamera camera = new PerspectiveCamera( true );
camera.setFarClip( 300 );
camera.setTranslateZ( -50 );
scene.setCamera( camera );
// 光源設定
LightBase light = new PointLight();
light.setTranslateY( -15 );
light.setTranslateZ( -25 );
root.getChildren().add( light );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
}
/**
* 単純に色を適用
* @return
*/
public Node createColorSphere()
{
// 球体を作成
Sphere sphere = new Sphere( 3 );
sphere.setRotationAxis( new Point3D( 1 , 1 , 0 ) );
sphere.setRotate( 30 );
sphere.setTranslateX( -12 );
sphere.setTranslateY( -5 );
// 色を設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor( Color.web( "9FCC7F" ) );
material.setSpecularColor( Color.WHITE );
// マテリアルを設定
sphere.setMaterial( material );
return sphere;
}
/**
* バンプマップ(法線マップ)の利用
* @return
*/
public Node createBumpMapSphere()
{
// 球体を作成
Sphere sphere = new Sphere( 3 );
sphere.setRotationAxis( new Point3D( 1 , 1 , 0 ) );
sphere.setRotate( 30 );
sphere.setTranslateX( -4 );
sphere.setTranslateY( -5 );
// バンプマップを読込
String url = new File( "img/bumpMap.png" ).toURI().toString();
Image bumpImg = new Image( url );
// テクスチャを設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor( Color.web( "9FCC7F" ) );
material.setBumpMap( bumpImg );
// マテリアルを設定
sphere.setMaterial( material );
return sphere;
}
/**
* スペキュラマップの利用
* @return
*/
public Node createSpecularMapSphere()
{
// 球体を作成
Sphere sphere = new Sphere( 3 );
sphere.setRotationAxis( new Point3D( 1 , 1 , 0 ) );
sphere.setRotate( 30 );
sphere.setTranslateX( 4 );
sphere.setTranslateY( -5 );
// スペキュラマップを読込
String url = new File( "img/specularMap.png" ).toURI().toString();
Image specularImg = new Image( url );
// テクスチャを設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor( Color.web( "9FCC7F" ) );
material.setDiffuseMap( specularImg );
material.setSpecularMap( specularImg );
material.setSpecularColor( Color.WHITE );
material.setSpecularPower( 5 );
// マテリアルを設定
sphere.setMaterial( material );
return sphere;
}
/**
* 自己照明マップの利用
* @return
*/
public Node createSelfIlluminationMapSphere()
{
// 球体を作成
Sphere sphere = new Sphere( 3 );
sphere.setRotationAxis( new Point3D( 1 , 1 , 0 ) );
sphere.setRotate( 30 );
sphere.setTranslateX( 12 );
sphere.setTranslateY( -5 );
// 自己照明マップを読込
String url = new File( "img/selfIlluminationMap.png" ).toURI().toString();
Image selfIlluminationImg = new Image( url );
// テクスチャを設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor( Color.web( "9FCC7F" ) );
material.setSelfIlluminationMap( selfIlluminationImg );
// マテリアルを設定
sphere.setMaterial( material );
return sphere;
}
/**
* テクスチャを適用した立方体を作成
* @return
*/
public Node createBox( Image img )
{
// 立方体を作成
Box box = new Box( 5.0 , 5.0 , 5.0 );
box.setRotationAxis( new Point3D( 1 , 1 , 0 ) );
box.setRotate( 30 );
box.setTranslateX( -12 );
box.setTranslateY( 5 );
// テクスチャを設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap( img );
// マテリアルを設定
box.setMaterial( material );
return box;
}
/**
* テクスチャを適用した球体を作成
* @return
*/
public Node createSphere( Image img )
{
// 球体を作成
Sphere sphere = new Sphere();
sphere.setRadius( 3.0 );
sphere.setTranslateX( -4 );
sphere.setTranslateY( 5 );
// テクスチャを設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap( img );
// マテリアルを設定
sphere.setMaterial( material );
return sphere;
}
/**
* テクスチャを適用した円柱を作成
* @return
*/
public Node createCylinder( Image img )
{
// 球体を作成
Cylinder cylinder = new Cylinder( 3 , 5 );
cylinder.setRotationAxis( new Point3D( 1 , 1 , 0 ) );
cylinder.setRotate( 30 );
cylinder.setTranslateX( 4 );
cylinder.setTranslateY( 5 );
// テクスチャを設定
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap( img );
// マテリアルを設定
cylinder.setMaterial( material );
return cylinder;
}
/**
* トライアングル・メッシュを作成
*
* 【メッシュ】
* p0┏━┓p3
* ┃\┃
* p1┗━┛p2
*
* 【テクスチャ】
* t0┏━┓t3
* ┃ ┃
* t1┗━┛t2
*
* @return
*/
public Node createTriangleMesh( Image img )
{
// メッシュビューを作成
MeshView meshView = new MeshView();
meshView.setTranslateX( 12 );
meshView.setTranslateY( 5 );
// メッシュを作成
TriangleMesh mesh = new TriangleMesh();
float[] points = { -2 ,-2 ,0 , // p0
-2 ,2 ,0 , // p1
2 ,2 ,0 , // p2
2 ,-2 ,0 }; // p3
float[] texCoords = { 0 , 0 , // t0
0 , 1 , // t1
1 , 1 , // t2
1 , 0 }; // t3
int[] faces = { 0 , 0 , 1 , 1 , 2 , 2,
2 , 2 , 3 , 3 , 0 , 0 };
mesh.getPoints().addAll( points );
mesh.getTexCoords().addAll( texCoords );
mesh.getFaces().addAll( faces );
// マテリアルを作成
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap( img );
// メッシュを登録
meshView.setMesh( mesh );
meshView.setMaterial( material );
return meshView;
}
}
◇リソース
(bumpMap.png)
(specularMap.png)
(selfIlluminationMap.png)
(diffuseMap.jpg)
◇実行結果
◇解説
プログラムの骨子はstart関数(30行目~69行目)であり、ウィンドウ表示処理を行っている。マテリアル利用例は各create○○関数であり、各create○○関数で作成した3Dオブジェクトは39行目~48行目でシーングラフに登録している。以下では、マテリアル設定項目別に解説する。
色の利用(上段の左)
色の設定はcreateColorSphere関数(76行目~94行目)で行っている。空のマテリアルを作成した後(86行目)、etDiffuseColor関数で物体色を設定している。
バンプマップの利用(上段の中央左)
バンプマップの設定はcreateBumpMapSphere関数(100行目~122行目)で行っている。面の法線をバンプマップで指定することで(116行目)、物体表面に凹凸があるように影を発生させる。ただし、実際に凹凸があるわけではないので物体の輪郭は変わらない。
バンプマップは画像であり、1ピクセルのRGB値が法線の方向(u,v,zの3次元)を表す。正式な説明ドキュメントは発見できなかったが、バンプマップ画像上の各ピクセルのRGB値と法線方向は以下のように対応している模様。
- R要素 ・・・テクスチャのUV座標におけるUの方向を表す。R=0がU=-1、R=128がU=0、R=255がU=1に対応
- G要素 ・・・テクスチャのUV座標におけるVの方向を表す。G=0がV=-1、G=128がU=0、G=255がV=1に対応
- B要素 ・・・物体の法線方向を表す。B=0が0、B=255が1に対応する
サンプルのバンプマップ(bumpMap.png)についてみていくと、大部分を占める青紫:RGB=(128,128,255)は法線(0,0,1)(=通常の法線)を表しているため見た目に変化はない。しかし、四角形の上辺(水色:RGB=(128,255,255))は法線(0,1,1)を表し、『手前下』向きの法線を表すため、実行結果画面で見ると面の方向が手前下向きになっている。
反射光の利用(上段の中央右)
反射光の設定はcreateSpecularMapSphere関数(129行目~154行目)で行っている。反射光の色を設定することで(147行目)、反射色が描画されるようになる。反射の強さはデフォルト値が32で、値が小さいほど反射する領域が広くなる(148行目)。SpecularMapはモノクロの画像であり、白:RGB=(255,255,255)の領域では設定した反射の強さ(=SpecularPower)がそのまま適用され、黒:RGB=(0,0,0)の領域では全く反射されない。
分かりにくいので、以下にSpecularMapを利用する場合と利用しない場合の画像を示す。利用しない場合(左)では物体表面の色に関係なく反射が発生しているが、利用する場合(右)では黒色の部分では全く反射が発生していない。
SpecularMapなし(左)、SpecularMapあり(右)
自己照明の利用(上段の右)
自己照明の設定はcreateSelfIlluminationMapSphere関数(160行目~182行目)で行っている。自己照明マップはモノクロの画像で、白:RGB=(255,255,255)の領域は発光し、黒:RGB=(0,0,0)の領域は発光しない。ちなみに、自己照明の発光は他のオブジェクトには影響を与えない模様で、発光部分に影がなくなる程度の効果しかない。
テクスチャの利用(下段)
テクスチャの利用は、createBox関数~createTriangleMesh関数(188行目~300行目)で行っている。メッシュ以外の場合、1つの面にテクスチャ全体が表示される模様。メッシュの場合は、オブジェクトの頂点とテクスチャ画像座標を対応付ける必要がある(276行目~289行目)。
テクスチャ画像の座標はuv座標系で表す。uv座標系はテクスチャ画像の左上を(0,0)とし、右に向かうにつれu値が、下に向かうにつれv値が大きくなっていく。u値、v値ともに範囲は0~1である。
メッシュのサンプルではオブジェクトの頂点を宣言(277行目~280行目)した後、テクスチャ座標を宣言(281行目~284行目)している。テクスチャ座標は(u,v)の組を1次元配列に並べて格納する。そして、面の作成時(285行目~286行目)に、(頂点番号,テクスチャ番号)の組を1次元配列に並べて格納している。
■ 参照
- JavaDoc - クラスPhongMaterial
- Bump and Normal Maps
- JavaFX の標準機能だけでシンプルな 3D トイピアノをつくろう