リニアングラフ3Dは、Ver.5.6以降において、Java※1言語での制御もサポートしています。 ここでは、実際にJava言語のコードから3Dグラフをプロットさせたり、3D図形を描画させたりする方法を解説します。
Java言語でリニアングラフ3Dを制御するには、別途Java言語の開発環境(JDK)が必要です。 JDKの入手やインストールについては、Java言語の解説書や解説サイトなどをご参照ください。 なお、ここでは解説の単純化のためIDEは使用せず、コンパイルと実行はコマンドラインで行います。
はじめに、環境が揃っている事の確認として、リニアングラフ3Dを起動するための簡単なサンプルを作成し、コンパイル・実行してみましょう。 「 Sample0.java 」という名前で、以下のコードを記述したテキストファイルを作成してください:
import com.rinearn.graph3d.RinearnGraph3D;
public class Sample0 {
public static void main(String[] args) {
// グラフを起動
RinearnGraph3D graph = new RinearnGraph3D();
}
}
code/Sample0.java
続いて、リニアングラフ3Dの配布パッケージ内にある「 RinearnGraph3D.jar (JARファイル) 」を、 上のコードと同じ場所に置いてください。 そして、コマンドライン端末上でその場所に cd などで移動し、以下のように入力してコンパイルしてください (Microsoft Windows※2 をご使用の場合と Linux※3 等をご使用の場合で異なります):
エラー無くコンパイルできた事を確認した上で、そのまま以下のように入力して実行してください:
実行の結果、グラフ画面が起動すれば成功です。 以降のサンプルコードでは、上のコマンドのSample0の部分をSample1〜Sample6に置き換えて、同様にコンパイル・実行できます。 その際、ファイル名は必ず「 クラス名.java 」とするようにご注意ください。
この解説で使用するリニアングラフ3DのJava言語用APIライブラリは、以下のURLで仕様書を参照できます。 この解説では触れきれない細かい機能の一覧や詳細などは、そちらをご参照ください:
APIライブラリの中でも特に使用するRinearnGraph3Dクラスの仕様書は、以下で参照できます:
以下では、いくつかの基本となる使い方について、サンプルコードを例示しながら解説していきます。 なお、以下で掲載するサンプルコードは、配布パッケージ内にも同梱されています。
まずは、実用における最も単純な例として、CSVファイルに記載された座標値データをプロットしてみましょう。以下のサンプルデータファイルを使用します:
- SampleDataFile1.csv -
この通り、3カラム書式(左から x y z )で、0 <= x <= 10の範囲で適当に座標値が書き出されています。 このファイルを開いてグラフにプロットするプログラムは以下の通りです:
import com.rinearn.graph3d.RinearnGraph3D;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Sample1 {
public static void main(String[] args) {
// グラフを起動
RinearnGraph3D graph = new RinearnGraph3D();
try {
// データファイルを読み込んでプロット
graph.openDataFile(new File("SampleDataFile1.csv"));
// 異常時の例外処理
} catch (FileNotFoundException fnfe) {
System.err.println("ファイルが見つかりませんでした。");
} catch (IOException ioe) {
System.err.println("ファイルが開けませんでした。");
}
}
}
code/Sample1.java
上のコードでは、まずRinearnGraph3Dクラスのインスタンスを生成し(この時点でグラフ画面が起動します)、そして openDataFile メソッドを呼び出して、開きたいファイルを引数に渡しています。 ファイルを開く過程での例外については、必要に応じて適切に処理してください。
ファイルを介さず、配列に格納された座標値データを、直接渡してプロットする事もできます:
import com.rinearn.graph3d.RinearnGraph3D;
public class Sample2 {
public static void main(String[] args) {
// 座標値データの用意
int n = 300;
double[] x = new double[n];
double[] y = new double[n];
double[] z = new double[n];
for(int i=0; i<n; i++) {
x[i] = i*0.1;
y[i] = Math.sin(x[i]);
z[i] = Math.cos(x[i]);
}
// グラフにプロット
RinearnGraph3D graph = new RinearnGraph3D();
graph.setData(x, y, z);
}
}
code/Sample2.java
このように、先ほどファイルを開いていた openDataFile メソッドの代わりに、 setData メソッドで配列データを渡します。
配列データは、ここでは単系列なので x[ 座標点インデックス ] の形ですが、 系列を分けたい場合は x[ 小系列インデックス ][ 座標点インデックス ] や x[ 大系列インデックス ][ 小系列インデックス ][ 座標点インデックス ] の形の多次元配列にします(y, zも同様)。 線プロット時には、小系列の区切りで線が切れ、大系列の区切りで色が変わります。
なお、メッシュプロットや曲面プロットの場合は、 x[ メッシュ縦方向インデックス ][ メッシュ横方向インデックス ] のように、メッシュの縦横2方向のインデックスを持たせてください(y, zも同様)。 その際、どちらが縦か横かは気にしなくても構いません。具体例は「範囲やオプションなどの詳細設定」の項をご参照ください。
グラフを画像ファイルに出力するには、RinearnGraph3Dクラスの exportImageFile メソッドを使用します。 例えば、これまで登場したサンプルコードにおいて、mainメソッドの末尾に以下の一行を追加すれば、画像ファイル「graph.png」が出力されます:
try {
graph.exportImageFile(new File("graph.png"), 1.0);
} catch (IOException ioe) {
System.err.println("ファイル出力に失敗しました。");
}
code/ExportImage.java
画像形式は拡張子から自動で判断されますが、現時点ではJPEGとPNG形式にのみ対応しています。 2番目の引数は画質で、1.0で最高、0.0で最低となります(JPEG形式のみで有効です)。
なお、getImage メソッドにより、グラフ画像をjava.awt.Imageクラスのインスタンスとして取得する事もできます。 これにより、グラフ画像を加工したり、別のGUI画面上に埋め込んだりする事も(それなりの処理を書く必要がありますが)可能です。
setXRange / setYRange / setZRange メソッドや setOptionSelected メソッドなどを用いて、プロット範囲やオプションなどの設定を行う事ができます。 例として、「配列データのプロット」項の最後で触れたメッシュ状の配列データをプロットし、メッシュプロットオプションを有効にして、範囲も設定してみましょう:
import com.rinearn.graph3d.RinearnGraph3D;
import com.rinearn.graph3d.RinearnGraph3DOptionItem;
public class Sample3 {
public static void main(String[] args) {
// 座標値データの用意
int n = 80; // メッシュの各方向の区間数
double[][] x = new double[n+1][n+1]; //各方向の頂点数は区間数+1
double[][] y = new double[n+1][n+1];
double[][] z = new double[n+1][n+1];
for(int i=0; i<=n; i++) {
for(int j=0; j<=n; j++) {
x[i][j] = i * 0.1;
y[i][j] = j * 0.1;
z[i][j] = Math.sin(x[i][j]) * Math.sin(y[i][j]);
}
}
// グラフを起動してデータをプロット
RinearnGraph3D graph = new RinearnGraph3D();
graph.setData(x, y, z);
// オプション設定(点プロット無効化して曲面プロット有効化)
graph.setOptionSelected(RinearnGraph3DOptionItem.POINT, false);
graph.setOptionSelected(RinearnGraph3DOptionItem.MESH, true);
// 範囲設定
graph.setXRange(0.0, 5.0);
graph.setYRange(0.0, 5.0);
graph.setZRange(-1.0, 1.0);
}
}
code/Sample3.java
上のコードでは、setOptionSelectedメソッドでプロットオプションの有効/無効状態を操作しています。 操作するオプションの項目はRinearnGraph3DOptionItem列挙子の要素で指定します。 上では初期状態で有効な点プロットオプションを無効化し、メッシュプロットオプションを有効化しています。 また、setXRange / setYRange / setZRange メソッドでそれぞれX/Y/Z軸方向のプロット範囲を設定しています。
設定系のメソッドは他にも存在します。詳細については、RinearnGraph3DクラスのAPI仕様書をご参照ください。 なお、細かい設定を一括で行うには、リニアングラフ3Dの設定ファイルを loadConfigurationFile メソッドで読み込む方法もあります。
アニメーションプロットを行うには、スレッドを生成して、その中で 「ファイルのプロット」や「配列データのプロット」 の項で扱ったファイルや配列データのプロットを繰り返し、少しずつ異なる内容を連続で描かせ続ければOKです。
ただし標準では、setData メソッドで配列データを渡した際、データの描画処理が完了するまで、呼び出し元に処理が戻らない事に留意する必要があります。 これは、アニメーションの各画面をコマ落ちなく画像に出力したい場合などには便利です。 しかし、例えば外部の装置などから入力されるデータをリアルタイムでプロットしたい場合などには、 次々と入ってくるデータに対して描画速度が追い付かない、つまり描画がボトルネックになってしまう可能性もあります。
そのような場合には、 setAsynchronousPlottingEnabled メソッドの引数にtrueを指定すると、 setData メソッド呼び出し時はすぐに処理が戻り、描画処理は別スレッドで非同期に、適当なタイミングで行われるようになります。 これは、一般にアニメーション速度を描画所要時間に依存させたくない場合にも有効です。
実際に、毎時刻の配列データを計算で生成し、非同期描画でアニメーションさせてみましょう:
import com.rinearn.graph3d.RinearnGraph3D;
import com.rinearn.graph3d.RinearnGraph3DOptionItem;
import java.awt.event.WindowListener;
import java.awt.event.WindowEvent;
public class Sample4 implements Runnable, WindowListener {
private Thread thread = null; // アニメーション用のスレッド
private RinearnGraph3D graph = null; // グラフ
private volatile boolean loopState = true; // ループの継続/終了を制御する
// mainメソッド(最初に実行されます)
public static void main(String[] args) {
new Sample4();
}
// 初期化・実行開始処理
public Sample4() {
// グラフを起動してプロットオプションを設定(曲面プロット)
this.graph = new RinearnGraph3D();
this.graph.setOptionSelected(RinearnGraph3DOptionItem.POINT, false);
this.graph.setOptionSelected(RinearnGraph3DOptionItem.MEMBRANE, true);
// 描画範囲の設定と自動調整機能の無効化
this.graph.setXRange(0.0, 5.0);
this.graph.setYRange(0.0, 5.0);
this.graph.setZRange(-1.0, 1.0);
this.graph.setXAutoRangingEnabled(false);
this.graph.setYAutoRangingEnabled(false);
this.graph.setZAutoRangingEnabled(false);
// データ更新と描画処理の関係を非同期にする(リアルタイムアニメーション用)
//(※ 連番で画像出力する用途などでは行わない方がコマ落ちや欠けを防げる)
this.graph.setAsynchronousPlottingEnabled(true);
// グラフを閉じたら独自終了処理を行うリスナーを登録、デフォルト処理は無効化
this.graph.addWindowListener(this);
this.graph.setAutoDisposingEnabled(false);
// アニメーションスレッドを生成して実行開始
this.thread = new Thread(this);
this.thread.start();
}
// アニメーションスレッドの処理
@Override
public void run() {
int n = 80; // メッシュの各方向の区間数
double[][] x = new double[n+1][n+1]; //各方向の頂点数は区間数+1
double[][] y = new double[n+1][n+1];
double[][] z = new double[n+1][n+1];
// アニメーションループ(continuesLoopがtrueの間継続)
for(int frame=0; this.continuesLoop; frame++) {
double t = frame * 0.05; // 時刻変数
// 座標値データの更新
for(int i=0; i<=n; i++) {
for(int j=0; j<=n; j++) {
x[i][j] = i * (5.0/n);
y[i][j] = j * (5.0/n);
z[i][j] = Math.sin(x[i][j]-t) * Math.cos(y[i][j]+t)
* Math.sin(Math.cos(x[i][j]+y[i][j])-2*t-0.7);
}
}
// 座標値データをグラフに転送(非同期)
this.graph.setData(x, y, z);
// 50ミリ秒だけ停止(時間は適時調整)
try {
this.thread.sleep(50);
} catch(InterruptedException e) {
// 割り込み例外の処理
}
}
// アニメーションループが終了したらグラフを破棄
this.graph.dispose();
// スレッド処理終端: 他にスレッド・リソースが残っていなければ自然に実行終了
}
// ※ 以下のようなイベント処理の実装が面倒な場合は、多少強引でよければ、
// this.graph.setAutoExittingEnabled(true); により、グラフを閉じたら
// アプリケーションの実行を即終了するよう設定可能です(即席の場合向けです)。
// グラフのウィンドウが閉じられた際に行うイベント処理
@Override
public void windowClosing(WindowEvent e) {
// アニメーションループを脱出させ、スレッドを終了させる(結果、実行も終了)
this.continuesLoop = false;
}
// その他のウィンドウイベント処理(ここでは何もしない)
@Override
public void windowDeactivated(WindowEvent e) { }
@Override
public void windowActivated(WindowEvent e) { }
@Override
public void windowDeiconified(WindowEvent e) { }
@Override
public void windowIconified(WindowEvent e) { }
@Override
public void windowOpened(WindowEvent e) { }
@Override
public void windowClosed(WindowEvent e) { }
}
code/Sample4.java
ここまで、座標値データから、点や線および面などを組み合わせて3Dのグラフを描く処理は、全てリニアングラフ3D側が自動で行っていました。 これは手軽なので便利ですが、用途によっては、自分で自由に、3D空間内に点や線および面などを描きたい場合もあるでしょう。
そのような場合には、リニアングラフ3Dの描画エンジンのAPIを使用します。 これは RinearnGraph3DRenderer クラスの各メソッドとして提供されています。このクラスの仕様書は下記で参照できます:
このAPIを用いると、リニアングラフ3Dを通常のグラフソフトとしてだけではなく、プログラミングでの簡易3D描画環境としても活用する事ができます。 特に、表示ウィンドウやマウスでの視点操作機能などが最初からトータルで備わっているので、「 とにかく即席で3D図形を描画したい 」といった用途などに便利かもしれません。
リニアングラフ3Dでは、全処理をJava言語で実装した、ソフトウェアレンダリング式の3D描画エンジンを採用しています。 これにより、動作環境に関する要求は特に無く、どこでも概ね同様の描画結果が得られます。
その半面として、描画速度は数十万ポリゴン/秒程度と、そう速くありません。また、描画形式はシンプルなZソート形式のフラットシェーディングのみとなっています。 遠近判定はピクセル単位ではなくポリゴン単位で行われるため、ポリゴン同士が交差・めり込んでいたり、遠近方向にかなり近接していたり、巨大なポリゴンがある箇所などでは、描画上の手前/奥の関係がラフになります。 そのため、細かく遠近判定させたい箇所では、ポリゴンも細かく分割してください。
さて、描画エンジンの処理は、もちろんリニアングラフ3D側からも呼び出されます。 そのため、ここで扱う描画エンジンの直接操作と、リニアングラフ3Dの他の機能(ファイルを開いてプロットしたり、メニューから手動でグラフ範囲やプロットオプションを変更したり、等々)とを併用したい場合には、少し工夫が必要です。 詳しくは次の項で説明しますが、とりあえずそのような事を一切考えずに、単に3D空間に図形を描きたいだけであれば、非常に単純にコードを書く事ができます。
まずは最も簡単な例として、3D空間内の自由な位置に、点と線、三角形、および四角形を1個ずつ描いてみましょう:
import com.rinearn.graph3d.RinearnGraph3D;
import com.rinearn.graph3d.renderer.RinearnGraph3DRenderer;
import java.awt.Color;
public class Sample5 {
public static void main(String[] args) {
// グラフを起動してレンダラー(描画エンジン)を取得
RinearnGraph3D graph = new RinearnGraph3D();
RinearnGraph3DRenderer renderer = graph.getRenderer();
// グラフ空間の範囲を設定
graph.setXRange(1.0, 10.0);
graph.setYRange(1.0, 10.0);
graph.setZRange(1.0, 10.0);
// グラフ空間内の(1,2,3)の位置に、半径10ピクセルで赤色の点を描画
renderer.drawPoint(1.0,2.0,3.0, 10.0, Color.RED);
// (1,1,1)と(8,8,5)の位置を結ぶ、太さ5ピクセルで緑色の線を描画
renderer.drawLine(1.0,1.0,1.0, 8.0,8.0,5.0, 5.0, Color.GREEN);
// (1,4,5)と(5,4,5)と(5,8,7)を結ぶ、青色の三角形を描画
renderer.drawTriangle(1.0,4.0,5.0, 5.0,4.0,5.0, 5.0,8.0,7.0, Color.BLUE);
// (5,1,1)と(8,1,1)と(8,4,1)と(5,4,1)を結ぶ、紫色の四角形を描画
renderer.drawQuadrangle(
5.0,1.0,1.0, 8.0,1.0,1.0, 8.0,4.0,1.0, 5.0,4.0,1.0, Color.MAGENTA
);
// スクリーンの再描画(3DCGレンダリング)
renderer.render();
}
}
code/Sample5.java
上のコードでは、まずグラフを生成して getRenderer メソッドで描画エンジンを取得し、グラフ空間の範囲を設定した上で、描画エンジンの各描画メソッドを呼び出して点や線などを描いています。
最後に、render メソッドでスクリーンの再描画を行わせていますが、この段階で初めて、3D空間内に描いた立体が、平面のグラフ画面に射影されて(2次元の絵として)描画されます。いわゆる3DCGのレンダリング処理です。 マウスで視点を操作した際などには自動で再レンダリングされますが、点や線の描画メソッドを呼ぶ度に毎回自動で再レンダリングされたりはしないため(処理コストが大きいためです)、このように最後に明示的に再レンダリングさせています。
drawPoint などの各描画メソッドの引数に渡す座標は、標準ではグラフ空間における座標と見なされます。 そのため、描画される絶対的な位置はグラフの範囲設定によって変化します。 また、グラフの範囲より外側にはみ出した内容は、標準では描画されません。このような細かい挙動を変更するには、各描画メソッドの引数で色を指定している部分で、 代わりに RinearnGraph3DDrawingParameter クラスのインスタンスを渡してください。 このクラスには、描画時のグラフ範囲に応じたスケーリングの有無や、はみ出した部分の除去の有無、および色設定や自動彩色の有無などを細かく設定できます。
なお、最初にRinearnGraph3DRendererクラスの clear メソッドを呼び出しておくと、グラフ範囲の目盛りや枠線を非表示にする事もできます。
ところで、上のサンプルコードでは、後からグラフ範囲やプロットオプションの変更操作などを行うと、描画内容が消えてしまいます。 そのような操作は、グラフ全体の立体形状の造りなおしが必要になるため、最初にリニアングラフ3D側から描画内容をリセットする処理が呼ばれるからです。
といっても、別にグラフ範囲やプロットオプションの変更を行う必要が無く、そもそもユーザーに画面上でそのような操作をさせたくない場合であれば、 RinearnGraph3Dクラスの setMenuVisibleメソッドの引数にfalseを指定して、メニューを非表示にする事もできます。
一方で、ちゃんとグラフ範囲の変更などに対応したい場合は、グラフの再描画が必要なタイミングをイベントとして受け取って、その度に描画しなおす事もできます。以下がその例です:
import com.rinearn.graph3d.RinearnGraph3D;
import com.rinearn.graph3d.renderer.RinearnGraph3DRenderer;
import com.rinearn.graph3d.event.RinearnGraph3DPlottingEvent;
import com.rinearn.graph3d.event.RinearnGraph3DPlottingListener;
import java.awt.Color;
public class Sample6 implements RinearnGraph3DPlottingListener {
RinearnGraph3D graph;
RinearnGraph3DRenderer renderer;
public static void main(String[] args) {
new Sample6();
}
// グラフの起動と初期設定
public Sample6() {
// グラフを起動してレンダラー(描画エンジン)を取得
this.graph = new RinearnGraph3D();
this.renderer = this.graph.getRenderer();
// グラフ空間の範囲を設定
this.graph.setXRange(1.0, 10.0);
this.graph.setYRange(1.0, 10.0);
this.graph.setZRange(1.0, 10.0);
// 再描画が必要になったらイベントで受け取れるようにリスナー登録
this.graph.addPlottingListener(this);
// 3D形状の描画処理を実行してスクリーンを3DCGレンダリング
this.draw();
this.renderer.render();
}
// 3D形状の描画処理
public void draw() {
// グラフ空間内の(1,2,3)の位置に、半径10ピクセルで赤色の点を描画
this.renderer.drawPoint(1.0,2.0,3.0, 10.0, Color.RED);
// (1,1,1)と(8,8,5)の位置を結ぶ、太さ5ピクセルで緑色の線を描画
this.renderer.drawLine(1.0,1.0,1.0, 8.0,8.0,5.0, 5.0, Color.GREEN);
// (1,4,5)と(5,4,5)と(5,8,7)を結ぶ、青色の三角形を描画
this.renderer.drawTriangle(1.0,4.0,5.0, 5.0,4.0,5.0, 5.0,8.0,7.0, Color.BLUE);
// (5,1,1)と(8,1,1)と(8,4,1)と(5,4,1)を結ぶ、紫色の四角形を描画
this.renderer.drawQuadrangle(
5.0,1.0,1.0, 8.0,1.0,1.0, 8.0,4.0,1.0, 5.0,4.0,1.0, Color.MAGENTA
);
}
// 再描画が必要になった際に呼ばれるイベント処理
@Override
public void plottingRequested(RinearnGraph3DPlottingEvent e) {
// 3D描画処理を再実行
this.draw();
}
// 以下のイベント処理はここでは何もしない
@Override
public void plottingCanceled(RinearnGraph3DPlottingEvent e) {
}
@Override
public void plottingFinished(RinearnGraph3DPlottingEvent e) {
}
}
code/Sample6.java
このコードを実行した結果、描画される内容は、先ほどのSample5と全く同様です。 しかし、メニューバーから 「編集」 > 「範囲の設定」 などを選んでグラフの範囲を変更しても、内容が消えてしまう事は無く、ちゃんと正しいグラフ範囲で表示されます。
これを実現するために、上のコードではまず、グラフの再描画が必要な(今の描画内容が消えてしまうような)タイミングをイベントとして受け取れるように、 イベントリスナーの RinearnGraph3DPlottingListener インターフェースを実装して、 リニアングラフ3Dに登録しています。そして、実際にイベントが発生した際に呼ばれる plottingRequested メソッド内で、3D描画処理を再実行して描き直しています。
ここで一つ注意が必要です。 RinearnGraph3DPlottingListenerインターフェースを実装する際、plottingRequestedメソッドなどの各イベント処理メソッド内から、 描画エンジンではなくリニアングラフ3D本体側である RinearnGraph3Dクラスのメソッドを呼ばないようにしてください (描画エンジンであるRinearnGraph3DRendererクラスのメソッドは呼んでもOKです)。
というのも、例えば plottingRequested メソッド内から、RinearnGraph3Dクラスの setXRange メソッドでグラフ範囲を変更すると、 それによって再描画が必要になるためplottingRequested が呼ばれ、またその中で setXRange メソッドを呼んで … と無限ループに陥ってしまいます。
他にも、RinearnGraph3Dクラスの多くのメソッドは、処理が中途半端なタイミングで行われてしまうのを避けるために、描画エンジンの操作中は一旦待機して、描画完了してから処理を行うようになっています。 そして、plottingRequested などの実行中は描画エンジンが操作中であると見なされるため、その中でRinearnGraph3Dクラスのメソッドを呼ぶと、描画完了をいつまでも待機してしまい、処理が進まなくなってしまいます。
RinearnGraph3DPlottingListener インターフェースを実装した結果、グラフが動かなくなってしまったら、上の点を踏まえて見直してみてください。