■Java 3D のイベント処理について -Behavior, Interpolator

Java 3D のイベント処理は、今までの Java のイベント処理とはかなり異なっています。

Java 3D のリテインド・モードでは、描画のスケジューリングは処理系に委ねられています。シーングラフがいったん "live" 状態になると、いつ何がどういう順序で描画されるのか、プログラマーが制御する方法はありません。

キーボード、マウスなどのイベント処理も、 この自動スケジューリング機能の制御下に置かれる必要があります。 従来のイベント・リスナーを使ったイベント処理や、 Thread を使ったアニメーションも 出来ないわけではありませんが、 Java 3D の自動スケジューリングと整合しない場合があります。

Java 3D では、イベント処理、アニメーションなどのために、従来とは違う処理モデルを導入しました。このためのクラスが javax.media.j3d.Behavior です。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.SceneGraphObject
        |
        +--javax.media.j3d.Node
              |
              +--javax.media.j3d.Leaf
                    |
                    +--javax.media.j3d.Behavior

クラス宣言

public abstract class Behavior
extends Leaf

コンストラクター

public Behavior()

Behavior は抽象クラスなので、実際にはそのサブクラスを使用します。キーボード、マウス操作のための Behaviorjavax.media.j3d パッケージでは提供されていません。

イベント処理のためには Behavior を継承したクラスを自分で書く必要があります。だたし、キーボード、マウス処理のための単純なクラスが com.sun.j3d.utils.behavior 配下のパッケージで提供されていますので、これを使うと良いでしょう。

Behavior の使用方法は次の通りです。

  1. Behavior の生成
  2. setSchedulingBounds(Bounds) で、この Behavior へのスケジューリングを有効にする領域を設定
  3. シーングラフに addChild() する

キーボード、マウスなどのイベントが発生すると、Behavior オブジェクトの processStimulus() メソッドが呼ばれます。BehaviorprocessStimulus()メソッドでイベントを処理します。例えばキーボードイベントなら、押されたキーを判定して視点を移動したり、マウスイベントなら、マウスの移動量を取得して物体を回転/移動させたりします。

Behavior はスケジューリングが有効になる領域を持っています。この領域の範囲外では Behavior は動作しません。この領域が設定されていないとき、Behavior は全く動作しませんsetSchedulingBounds() は必ず設定してください。

アニメーションには、Behavior のサブクラス javax.media.j3d.Interpolator を使用します。

java.lang.Object
  |
  +--javax.media.j3d.SceneGraphObject
        |
        +--javax.media.j3d.Node
              |
              +--javax.media.j3d.Leaf
                    |
                    +--javax.media.j3d.Behavior
                          |
                          +--javax.media.j3d.Interpolator

クラス宣言

public abstract class Interpolator
extends Behavior

コンストラクター

public Interpolator()

public Interpolator(Alpha alpha) // Alpha オブジェクト

Interpolator も抽象クラスなので、実際にはそのサブクラスを使用します。位置、大きさ、回転、色、透明度など様々なアニメーションのためのクラスが定義されています。

Interpolator によるアニメーションでは、時間による状態の変化を、javax.media.j3d.Alpha というクラスを使って定義します。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.SceneGraphObject
        |
        +--javax.media.j3d.NodeComponent
              |
              +--javax.media.j3d.Alpha

クラス宣言

public class Alpha
extends NodeComponent

コンストラクター

public Alpha()

public Alpha(int loopCount,                    // 繰り返しの回数
             int mode,                         // 変化モード(増加、減少、または両方)
             long triggerTime,                 // 最初の起動開始までのミリ秒
             long phaseDelayDuration,          // 最初の起動開始後に本当に変化を開始するまでのミリ秒
             long increasingAlphaDuration,     // 増加に要するミリ秒
             long increasingAlphaRampDuration, // 増加が加速するミリ秒
             long alphaAtOneDuration,          // 1.0 を保持するミリ秒
             long decreasingAlphaDuration,     // 減少に要するミリ秒
             long decreasingAlphaRampDuration, // 減少が加速するミリ秒
             long alphaAtZeroDuration)         // 0.0 を保持するミリ秒

public Alpha(int loopCount,                    // 繰り返しの回数
             long triggerTime,                 // 最初の起動開始までのミリ秒
             long phaseDelayDuration,          // 最初の起動開始後に本当に変化を開始するまでのミリ秒
             long increasingAlphaDuration,     // 増加に要するミリ秒
             long increasingAlphaRampDuration, // 増加が加速するミリ秒
             long alphaAtOneDuration)          // 1.0 を保持するミリ秒

Interpolator, Alpha については後に詳しく説明します。

では実際にキーボード、マウス処理をやってみましょう。

■キーボード、マウスを使った操作

■■キーボード操作による視点移動 (com.sun.j3d.utils.behaviors.keyboard.KeyNavigator)

com.sun.j3d.utils.behaviors.keyboard.KeyNavigatorBehavior を使うと、矢印キー(方向キー)による視点や物体の移動が容易に実現できます。


クラス宣言

public class KeyNavigatorBehavior
extends Behavior

コンストラクター

public KeyNavigatorBehavior(TransformGroup targetTG) // 移動対象の TransformGroup

KeyNavigatorBehavior のコンストラクターでは、移動対象の TransformGroup を指定します。物体を移動させる場合は、移動させたい物体を TransformGroupaddChild() して、物体の「親」になった TransformGroup を指定すれば良いですが、視点を移動させたい場合はどうしたら良いでしょうか。

SimpleUniverse を使ってシーン・グラフを構築しているときに、視点側の TransformGroup を取得するには次のようにします。

  TransformGroup viewtrans = universe.getViewingPlatform().getViewPlatformTransform();

まず、SimpleUniversegetViewingPlatform メソッドを使って、com.sun.j3d.utils.universe.ViewingPlatform オブジェクトを取得します。

SimpleUniverse のメソッド (一部)

public ViewingPlatform getViewingPlatform() // com.sun.j3d.utils.universe.ViewingPlatform を取得

つぎに、com.sun.j3d.utils.universe.ViewingPlatformgetViewPlatformTransform() メソッドを使って、視点側の TransformGroup を取得します。

com.sun.j3d.utils.universe.ViewingPlatform のメソッド (一部)

public TransformGroup getViewPlatformTransform() // 視点側の TransformGroup を取得

KeyNavigatorBehavior の使用方法は次の通りです。

  1. 移動対象の TransformGroup をコンストラクターで指定して KeyNavigatorBehavior を生成する
  2. setSchedulingBounds() メソッドで、スケジューリングが有効となる領域 (javax.media.j3d.Bounds オブジェクト) を設定する
  3. シーン・グラフに addChild() する

実行結果を見てください。

KeyNavigatorTest.gif

画面下部に、「床」となる QuadArray が置いてあります。

キーボードの矢印キー (↑↓←→) を使って、視点を前後に移動したり、左右に向きを変えたりできます。キー操作は次の通りです。

キー操作
前進
後退
左に向きを変える
右に向きを変える
PgUp下に向きを変える (ティルト・ダウン)
pgDn上に向きを変える (ティルト・アップ)
Alt + ←左に平行移動
Alt + →右に平行移動
Alt + PgUp上昇 (上方向に平行移動)
Alt + pgDn下降 (下方向に平行移動)
Shift + 他のキー移動量を大きくする

画面内にキー入力のフォーカスが無いと移動できません。移動できないときは画面内をマウスでクリックしてみてください。

テクスチャー・マッピングを使用したので、ウインドウを拡大すると動作が遅くなって行きます。あまり大きくしないでください。

ソースコードは次の通りです。

KeyNaviagtorTest.java
  1  // Java 3D Test Applet
  2  // KeyNavigatorTest.java
  3  //   Copyright (c) 1999 ENDO Yasuyuki
  4  //                      mailto:yasuyuki@javaopen.org
  5  //                      http://www.javaopen.org/j3dbook/index.html
  6  
  7  import java.applet.*;
  8  import java.awt.*;
  9  import javax.media.j3d.*;
 10  import javax.vecmath.*;
 11  import com.sun.j3d.utils.applet.MainFrame;
 12  import com.sun.j3d.utils.universe.SimpleUniverse;
 13  import com.sun.j3d.utils.universe.PlatformGeometry;
 14  import com.sun.j3d.utils.behaviors.keyboard.*;
 15  
 16  public class KeyNavigatorTest extends Applet {
 17    private SimpleUniverse universe = null;
 18  
 19    public KeyNavigatorTest() {
 20      GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
 21      Canvas3D canvas = new Canvas3D(config);
 22      this.setLayout(new BorderLayout());
 23      this.add(canvas, BorderLayout.CENTER);
 24  
 25      universe = new SimpleUniverse(canvas);
 26      universe.getViewingPlatform().setNominalViewingTransform();
 27      universe.getViewer().getView().setBackClipDistance(100.0); ................(1)
 28      BranchGroup scene = createSceneGraph();
 29    
 30      universe.addBranchGraph(scene);
 31    }
 32  
 33    private BranchGroup createSceneGraph() {
 34      BranchGroup root = new BranchGroup();
 35      
 36      BoundingSphere bounds = new BoundingSphere( new Point3d(), 100.0 );
 37  
 38      TransformGroup viewtrans =
 39        universe.getViewingPlatform().getViewPlatformTransform(); ...............(2)
 40      KeyNavigatorBehavior keybehavior = new KeyNavigatorBehavior(viewtrans); ...(3)
 41      keybehavior.setSchedulingBounds(bounds);...................................(4)
 42      PlatformGeometry vp = new PlatformGeometry();..............................(5)
 43      vp.addChild(keybehavior);..................................................(6)
 44      universe.getViewingPlatform().setPlatformGeometry(vp);.....................(7)
 45  
 46      root.addChild(createFloor());
 47      
 48      return root;
 49    }

(1)でjavax.media.j3d.ViewsetBackClipDistance() メソッドを使って、視点から奥のクリップ面 (ファー・クリップ面) までの距離を増大させています。

SimpleUniverse のデフォルトでは、視点からファー・クリップ面までの距離は 10.0 です。KeyNavigatorBehavior は移動できる距離が大きいので、SimpleUniverse のデフォルトの視野空間 (ビューイング・ボリューム) では狭かったためにこのような処理をしました。

(2)で視点側の TransformGroup を取得しています。

(3)で KeyNavigatorBehavior を生成しています。コンストラクターの引数に、視点側の TransformGroup を指定しています。この TransformGroup がキーボードによる移動の対象になります。

(4)でスケジューリングが有効になる領域 (BoundingShpere) を設定しています。

(5)でcom.sun.j3d.utils.universe.PlatformGeometryを生成しています。実は KeyNavigatorBehaviorはシーングラフのどこに追加しても動作します。動作はするのですが、ここではView側のツリーに追加してみます。

PlatformGeometryBranchGroupのサブクラスで、View側のツリー構築のために使用されます。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.SceneGraphObject
        |
        +--javax.media.j3d.Node
              |
              +--javax.media.j3d.Group
                    |
                    +--javax.media.j3d.BranchGroup
                          |
                          +--com.sun.j3d.utils.universe.PlatformGeometry

クラス宣言

public class PlatformGeometry
extends BranchGroup

コンストラクター

public PlatformGeometry()

SimpleUniverseを使って構築した視点側のツリーには、BranchGroupを動的に追加することはできません。BranchGroupではなくPlatformGeometryを生成し、com.sun.j3d.utils.universe.ViewingPlotformの次のメソッドを使って追加する必要があります。

com.sun.j3d.utils.universe.ViewingPlatformのメソッド

public void setPlatformGeometry(PlatformGeometry pg)

(6)で PlatformGeometryKeyNavigatorBehavioraddChild() しています。

(7)でPlatformGeometrycom.sun.j3d.utils.universe.ViewingPlatformにセットしています。

■■com.sun.j3d.utils.behaviors.mouse パッケージ

com.sun.j3d.utils.behaviors.mouseパッケージのサンプルはすでに簡単に説明しています。ここではマウス操作で変更された TransformGroupの状態を取得する方法について調べてみましょう。

■■マウス操作で変更された TransformGroup の状態を取得するには? (MouseBehaivorCallback を使ってみる)

MouseBehavior によるマウス操作で TransformGroup が変更されますが、この変化を取得する方法は無いでしょうか。

com.sun.j3d.utils.behaviors.mouse パッケージには、このための interface として MouseBehaviorCallback が用意されています。

public interface MouseBehaviorCallback

public static final int ROTATE    // MouseRotate による操作
public static final int TRANSLATE // MouseTraslate による操作
public static final int ZOOM      // MouseZoom による操作

public void transformChanged(int type,              // 回転/移動/ズームのどの操作か
                             Transform3D transform) // 変更された Transfom3D

MouseBehaivorCallback の使用方法は次のようになります。

  1. MouseBehaviorCallbackimplements したクラスを書く。transformChanged() メソッドで変更された Transform3D が取得できる。
  2. そのクラスを setupCallback() メソッドで MouseBehavior に設定する
  3. 実行時に MouseBehavior による操作が行われたとき transformChanged() メソッドが呼ばれる。

サンプルの実行結果を見てください。

QuatTest.gif

マウスの左ボタンで ColorCube を回転させると、ウインドウ下部の q.x, q.y, q.z, q.w の値が変化します。この値は、変化した Transform3D の回転要素を四元数 (Quotanion) として取得したものです。

四元数とは何でしょうか? 四元数を使うと回転をエレガントに表現でき、 計算が簡単になります。

四元数については巻末の「javax.vecmathパッケージ詳説」をお読みください。

QuatTest.java (一部)
 50    private BranchGroup createSceneGraph() {
 51      BranchGroup root = new BranchGroup();
 52      
 53      trans = new TransformGroup();
 54      trans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
 55      trans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
 56      trans.setCapability(TransformGroup.ENABLE_PICK_REPORTING);
 57  
 58      BoundingSphere bounds = new BoundingSphere(new Point3d(), 100.0);
 59      
 60      MouseRotate rotator = new MouseRotate(trans);
 61      rotator.setSchedulingBounds(bounds);
 62      rotator.setupCallback( new MouseBehaviorCallback() {               ┐
 63        public void transformChanged(int flag, Transform3D trans) { ┐    ├(1)
 64          trans.get(quat4f); ...................................(3) ├(2) │
 65          tuplePanel.set(quat4f); ..............................(4) │    │
 66        }                                                           ┘    │
 67      });                                                                ┘
 68      root.addChild(rotator);
 69      
 70      trans.addChild( new ColorCube(0.4) );
 71      
 72      root.addChild(trans);
 73      
 74      return root;
 75    }

(1)でMouseBehaviorCallbackimplements した内部クラスを定義し、setupCallback() メソッドで MouseRotate に設定しています。

MouseRotate を使って ColorCube を回転させたときにtransformChanged() メソッドが実行されます。

(2)が transformCharged() メソッドの定義です。(3)の get()Transform3D での回転をQuat4fに取得しています。取得した Quat4f を(4)で TuplePanelset() しています。

このサンプルでは回転を取得して表示しているだけですが、 取得したTransform3D を別のオブジェクトに適用することもできます。

■■com.sun.j3d.utlis.behaviors.picking

com.sun.j3d.utils.behaivors.mouse.MouseBehavior のサブクラス MouseRotate, MouseTranslate, MouseZoom は、適用対象の TransformGroup をあらかじめ設定しておきました。複数の物体を別個に回転/移動/ズームするにはどうしたら良いでしょうか。

このためには、マウスクリックなどで物体を選択し、 選択した物体に対して回転/移動/ズームなどの操作を適用する必要があります。

この、マウスのクリックなどで物体を選択することを「ピッキング」といいます。

Java 3D でピッキングを行うための概略は次の通りです。

  1. マウスクリックの際に、画面上のマウスの位置座標 (x, y) を取得する
  2. 画面上のマウスの位置座標を、3次元座標に変換する
  3. 変換された3次元座標から画面奥 (-Z方向) に向けて、ピッキングのための「光線」(Ray) を描画する
  4. Ray と交差した物体を BranchGroup のピッキングのためのメソッド pickAll(), pickAllSorted(), pickClosest(), pickAny() で調べる。戻り値として、交差した物体が格納された javax.media.j3d.SceneGraphPath オブジェクトが返される。

com.sun.j3d.utils.behaviors.picking パッケージでは、Ray と交差したもっとも手前の物体を取得し、その「親」の TransformGroup に対して回転/移動/ズームを適用します。複数の物体をそれぞれ別の TransformGroupaddChild() しておけば、ピックした物体をそれぞれ別個に回転/移動/ズームさせることができます。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.SceneGraphObject
        |
        +--javax.media.j3d.Node
              |
              +--javax.media.j3d.Leaf
                    |
                    +--javax.media.j3d.Behavior
                          |
                          +--com.sun.j3d.utils.behaviors.picking.PickMouseBehavior
                                |
                                |--com.sun.j3d.utils.behaviors.picking.PickRotateBehavior    // 回転
                                |
                                |--com.sun.j3d.utils.behaviors.picking.PickTranslateBehavior // 移動
                                |
                                +--com.sun.j3d.utils.behaviors.picking.PickZoomBehavior      // ズーム

クラス宣言

public abstract class PickMouseBehavior
extends Behavior

public class PickRotateBehavior
extends PickMouseBehavior
implements MouseBehaviorCallback

public class PickTranslateBehavior
extends PickMouseBehavior
implements MouseBehaviorCallback

public class PickZoomBehavior
extends PickMouseBehavior
implements MouseBehaviorCallback

コンストラクター (一部)

public PickMouseBehavior(Canvas3D canvas,  // 描画対象の Canvas3D
                         BranchGroup root, // ピック対象の BranchGroup
                         Bounds bounds)    // スケジューリングが有効になる領域      


public PickRotateBehavior(BranchGroup root,    // ピック対象の BranchGroup
                          Canvas3D canvas,     // 描画対象の Canvas3D
                          Bounds bounds,       // スケジューリングが有効になる領域      
                          int pickMode)        // ピッキングのモード       

public PickTranslateBehavior(BranchGroup root, // ピック対象の BranchGroup     
                             Canvas3D canvas,  // 描画対象の Canvas3D
                             Bounds bounds,    // スケジューリングが有効になる領域      
                             int pickMode)     // ピッキングのモード       

public PickZoomBehavior(BranchGroup root,      // ピック対象の BranchGroup 
                        Canvas3D canvas,       // 描画対象の Canvas3D      
                        Bounds bounds,         // スケジューリングが有効になる領域
                        int pickMode)          // ピッキングのモード       

com.sun.j3d.utils.behaviors.picking.PickObject の定数フィールド (一部)

public static final int USE_BOUNDS   // 境界(Bounds)でピックする

public static final int USE_GEOMETRY // 幾何学的形状を使ってピックする

PicRotateBehavior, PickTranslateBehavior, PickZoomBehaviorPickMouseBehavior のサブクラスです。

PickMouseBehavior を継承したクラスを書けば、回転/移動/ズーム以外の操作を行うことも可能です。(後の節では PickMouseBehavior を継承した簡単なサンプルを書いています)

ピッキングには 2種類のモードがあります。

物体のまわりの領域 (境界) を使ってピックするモード (PickObject.USE_BOUNDS を指定する) と、物体の幾何学的な形状 (ジオメトリ) を使ってピックするモード (PickObject.USE_GEOMETRY を指定する) です。デフォルトでは USE_BOUNDS です。

物体のまわりの境界 (Bounds) を使うモードでは、物体の周囲の矩形領域 (javax.media.j3d.BoundingBox) や球状の領域 (javax.media.j3d.BoundingSphere)、または 閉じたポリゴンの境界領域 (javax.media.j3d.BoundingPolytope) を使ってピッキングを行います。処理は USE_GEOMETRY モードでのピッキングよりも軽くなります。

ジオメトリを使うモードでは javax.media.j3d.Geometry オブジェクトで定義される物体の幾何学的な形状を使ってピッキングを行います。USE_BOUNDS モードよりも正確なピッキングが可能です。ただし、ピックする可能性のあるすべての GeometryALLOW_INTERSECT ビットを (setCapability() で) 設定する必要があります

サンプルの実行結果を見てください。

PickMouseTest.gif

複数の物体それぞれを回転/移動/ズームすることができます。マウスボタンは com.sun.j3d.utils.behaviors.mouse パッケージのときと同じです。

マウスボタン操作
左ドラッグ回転
中央ドラッグズーム (Z軸方向への移動)
右ドラッグ移動 (X軸、Y軸方向)

このサンプルのソースは次の通りです。

PickMouseTest.java (一部)
 48    private BranchGroup createSceneGraph() {
 49      BranchGroup root = new BranchGroup();
 50      
 51      BoundingSphere bounds = new BoundingSphere( new Point3d(), 100.0 );
 52      
 53      PickRotateBehavior rotator =
 54        new PickRotateBehavior(root, canvas, bounds, PickObject.USE_GEOMETRY); ...(1)
 55      root.addChild(rotator);.....................................................(2)
 56      
 57      PickTranslateBehavior translator =                                          ┐
 58        new PickTranslateBehavior(root, canvas, bounds, PickObject.USE_GEOMETRY); │
 59      root.addChild(translator);                                                  ├(3)
 60                                                                                  │
 61      PickZoomBehavior zoomer =                                                   │
 62        new PickZoomBehavior(root, canvas, bounds, PickObject.USE_GEOMETRY);      │
 63      root.addChild(zoomer);                                                      ┘
           :
           :
163    }

(1)で PickRotateBehavior を生成しています。コンストラクターの引数はピック対象の BranchGroup、描画に使用する Canvas3D、スケジューリングが有効になる領域 (Bounds)、ピッキングのモードです。ここではピッキングのモードに USE_GEOMETRY を指定しています。

(2)で BranchGroupPickRotateBehavioraddChild() しています。(3)ではこれと同様に PickTranslateBehavior, PickZoomBehavior の生成、addChild() を行っています。

ピッキングのためのBehaviorを生成し、 シーングラフに追加した後は、LineArray, Box, Cylinder, Cone, Sphere を生成し、capability bit の設定を行っています。このサンプルでは USE_GEOMETRY モードでピッキングを行っているので、つぎの設定が必要です。

対象capability bit意味
TransformGroupALLOW_TRANSFORM_READピック時に Transform3D を読み取る
ALLOW_TRANSFORM_WRITE回転/移動/ズームを適用する
ENABLE_PICK_REPORTINGピックされたことを"報告"する
GeometryALLOW_INTERSECT交差を許可する
Shape3DALLOW_GEOMETRY_READピック時に Geometry を読み取る
PrimitiveENABLE_GEOMETRY_PICKINGジオメトリでのピッキングを可能にする

■■ピックされた物体を特定する

ピックされたときに、回転/移動/ズーム以外の操作を行うにはどうしたら良いでしょうか。

前の節で書いた通り、com.sun.j3d.utils.behaviors.picking.PickMouseBehavior を継承したクラスを書けば独自のピッキング処理を行うことができます。

ここでは、ピックされた物体を特定する処理を書いてみましょう。

まず、ピッキングの際に実行されるメソッドを定義した interface を書きました。

SimplePickingCallback.java
 1 // Java 3Dテスト用プログラム
 2 // SimplePickingCallback.java
 3 //   Copyright (c) 1999 ENDO Yasuyuki
 4 //                      mailto:yasuyuki@javaopen.org
 5 //                      http://www.javaopen.org/j3dbook/index.html
 6 
 7 import javax.media.j3d.Node;
 8 
 9 public interface SimplePickingCallback {
10   void picked(int nodeType, Node node);
11 }

ピッキングの際に picked() メソッドを実行することにします。引数として渡すのは、ピックされた javax.media.j3d.Node の種類と、ピックされたNode オブジェクトです。

ピックされた Node の種類の指定には、PickObject の定数フィールドを使用します。

com.sun.j3d.utils.behaviors.picking.PickObject の定数フィールド (一部)

public static final int SHAPE3D         // javax.media.j3d.Shape3D オブジェクト
public static final int MORPH           // javax.media.j3d.Morph オブジェクト
public static final int PRIMITIVE       // com.sun.j3d.utils.geometry.Primitive オブジェクト
public static final int LINK            // javax.media.j3d.Link オブジェクト
public static final int GROUP           // javax.media.j3d.Group オブジェクト
public static final int TRANSFORM_GROUP // javax.media.j3d.TransformGroup オブジェクト
public static final int BRANCH_GROUP    // javax.media.j3d.BranchGroup オブジェクト
public static final int SWITCH          // javax.media.j3d.Switch オブジェクト

つぎに PickMouseBehavior を継承したクラスを書きます。

SimplePicking.java
 1 // Java 3Dテスト用プログラム
 2 // SimplePicking.java
 3 //   Copyright (c) 1999 ENDO Yasuyuki
 4 //                      mailto:yasuyuki@javaopen.org
 5 //                      http://www.javaopen.org/j3dbook/index.html
 6 
 7 import javax.media.j3d.*;
 8 import com.sun.j3d.utils.behaviors.picking.*;
 9 
10 public class SimplePicking extends PickMouseBehavior {
11   protected SimplePickingCallback callback = null;
12   protected int pickMode = PickObject.USE_BOUNDS;
13   protected int nodeType = PickObject.SHAPE3D;
14 
15   public SimplePicking(BranchGroup root, Canvas3D canvas, Bounds bounds) {
16     super(canvas, root, bounds);
17 
18     this.setSchedulingBounds(bounds);
19   }
20 
21   public SimplePicking( BranchGroup root, Canvas3D canvas, Bounds bounds,
22                         int mode )
23   {
24     super(canvas, root, bounds);
25     pickMode = mode;
26 
27     this.setSchedulingBounds(bounds);
28   }
29 
30   public SimplePicking( BranchGroup root, Canvas3D canvas, Bounds bounds,
31                         int mode, int type)
32   {
33     super(canvas, root, bounds);
34     pickMode = mode;
35     nodeType = type;
36 
37     this.setSchedulingBounds(bounds);
38   }
39 
40   public void setPickMode(int mode) { pickMode = mode; }
41   public int getPickMode() { return pickMode; }
42 
43   public void setNodeType(int type) { nodeType = type; }
44   public int getNodeType() { return nodeType; }
45 
46   public void setupCallback(SimplePickingCallback callback) { this.callback = callback; }
47 
48   public void updateScene(int x, int y) {
49     Node node = pickScene.pickNode(pickScene.pickClosest(x, y), nodeType);
50     callback.picked(nodeType, node);
51   }
52 }

PickMouseBehavior は、ピッキングの際に updateScene() メソッドを実行します。PickMouseBehavior のサブクラスでは、updateScene() メソッドを実装してピッキングの処理を書きます。

ここでは、マウスクリックした位置に最も近い Node を取得しています。このとき、Node の種類を指定していますが、この種類は SimplePicking のコンストラクターで与えたものです。デフォルトでは PickObject.SHAPE3D です。

クリックした位置に最も近い Node を取得したら、SimplePickingCallbackimplements したクラスの picked() メソッドを実行します。

以前のサンプルに SimplePicking を追加してみましょう。

PickMouseTest.java (一部)
 48    private BranchGroup createSceneGraph() {
 49      BranchGroup root = new BranchGroup();
 50      
 51      BoundingSphere bounds = new BoundingSphere( new Point3d(), 100.0 );
 52      
 53      PickRotateBehavior rotator =
 54        new PickRotateBehavior(root, canvas_, bounds, PickObject.USE_GEOMETRY);
 55      root.addChild(rotator);
 56      
 57      PickTranslateBehavior translator =
 58        new PickTranslateBehavior(root, canvas_, bounds, PickObject.USE_GEOMETRY);
 59      root.addChild(translator);
 60      
 61      PickZoomBehavior zoomer =
 62        new PickZoomBehavior(root, canvas_, bounds, PickObject.USE_GEOMETRY);
 63      root.addChild(zoomer);
 64      
 65     SimplePicking picker =                                                 
 66       new SimplePicking( root, canvas_, bounds,                            
 67                          PickObject.USE_GEOMETRY, PickObject.PRIMITIVE ); ...(1)
 68     picker.setupCallback( new SimplePickingCallback() {                    
 69       public void picked(int type, Node node) {                 ┐
 70         if (node != null) {                                ┐    │
 71           String data = (String)node.getUserData(); ...(4) │    │
 72           System.out.println(data);                        ├(3) ├(2)
 73         } else {                                           │    │
 74           System.out.println("Error: node is null.");      │    │
 75         }                                                  ┘    │
 76       }                                                         ┘
 77     });                                                                                     
 78     root.addChild(picker);........................................(5)
          :
          :
119      Transform3D bt3d = new Transform3D();
120      bt3d.set(new Vector3d(-0.4, 0.4, 0.0));
121      TransformGroup btrans = new TransformGroup(bt3d);
122      btrans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
123      btrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
124      btrans.setCapability(TransformGroup.ENABLE_PICK_REPORTING);
125      trans.addChild(btrans);
126  
127      Box box = 
128        new Box( 0.25f, 0.25f, 0.25f,
129                 Primitive.GENERATE_TEXTURE_COORDS |
130                 Primitive.ENABLE_GEOMETRY_PICKING,
131                 ap );
132      box.setUserData("this is box.");...............................(6)
133      btrans.addChild(box);
          :
          :
182    }

(1)で SimplePicking を生成しています。ピッキングは USE_GEOMETRY モード、ピックする物体の種類は PickObject.PRIMITIVE です。

(2)でSimplePickingCallbackimplements し た内部クラスを定義し、setupCallback() メソッドで SimplePicking に設定しています。 (3)が picked() メソッドの定義です。 (4)でgetUserData() メソッドを使って Node に追加されたユーザー・データを取得しています。このサンプルではユーザー・データは String です。

setUserData(), getUserData() は、 javax.media.j3d.Node のスーパー・クラスである javax.media.j3d.SceneGraphObject のメソッドです。

javax.media.j3d.SceneGraphObject のメソッド (一部)

public void setUserData(java.lang.Object userData) // ユーザー固有のデータ

public java.lang.Object getUserData() // ユーザー固有データを取得

SceneGraphObject のサブクラスでは、setUserData(), getUserData() を使ってユーザー固有のデータを設定/取得することができます。

(5)で SimplePickingBranchGroupaddChild() しています。

(6)でsetUserData() を使って String ("this is box.") を設定しています。 以下同様に、Cone, Cylinder, Sphereにも setUserData()Stringを設定しました。

実行結果は次の通りです。クリックした物体をコンソール表示します。

PickMouseTest.gif

$ java PickMouseTest
this is box.
this is cylinder.
this is cone.
this is sphere.
this is box.
this is cylinder.
this is cone.
this is sphere.

■■イベントリスナーを使う方法

従来の Java のイベントモデル、Delegation Listener モデルを使ったイベント処理も可能です。この場合、Canvas3D に対して add*Listener() してくださいContaineradd*Listener() してもイベントは通知されません。

サンプルソースは次の通りです。

PickSelectionFeedback.java
  1  // Java 3Dテスト用アプレット
  2  // PickSelectionFeedback.java
  3  //   Copyright (c) 1999 ENDO Yasuyuki
  4  //                      mailto:yasuyuki@javaopen.org
  5  //                      http://www.javaopen.org/j3dbook/index.html
  6  
  7  import java.awt.*;
  8  import java.awt.event.*;
  9  import javax.media.j3d.*;
 10  import javax.vecmath.*;
 11  import com.sun.j3d.utils.geometry.Primitive;
 12  import com.sun.j3d.utils.geometry.Cone;
 13  import com.sun.j3d.utils.geometry.Cylinder;
 14  import com.sun.j3d.utils.geometry.Box;
 15  import com.sun.j3d.utils.behaviors.picking.PickObject;
 16  import com.sun.j3d.utils.behaviors.picking.PickRotateBehavior;
 17  import com.sun.j3d.utils.behaviors.picking.PickTranslateBehavior;
 18  import com.sun.j3d.utils.behaviors.picking.PickZoomBehavior;
 19  import com.sun.j3d.utils.applet.MainFrame;
 20  import com.sun.j3d.utils.universe.*;
 21  
 22  public class PickSelectionFeedback extends java.applet.Applet {
 23    private Canvas3D canvas = null;
 24    private BranchGroup scene = null;
 25  
 26    public PickSelectionFeedback() {
 27      this.setLayout(new BorderLayout());
 28  
 29      GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
 30  
 31      canvas = new Canvas3D(config);
 32      this.add(canvas, BorderLayout.CENTER);
 33      
 34      //これでマウスイベントが通知される
 35      canvas.addMouseListener(new MouseAdapter() {               ┐
 36        public void mouseClicked( MouseEvent e ){           ┐    │
 37          Node node = pickNode(e.getX(), e.getY());.....(3) │    │
 38          if (node != null) {                               ├(2) │
 39            String data = (String)node.getUserData();...(4) │    ├(1) 
 40            System.out.println("UserData=" + data);.....(5) │    │
 41          }                                                 │    │
 42        }                                                   ┘    │
 43      });                                                        ┘
 44  
 45      SimpleUniverse universe = new SimpleUniverse(canvas);
 46      universe.getViewingPlatform().setNominalViewingTransform();
 47  
 48      scene = createSceneGraph();
 49  
 50      universe.addBranchGraph(scene);
 51    }
 52    
 53    private Node pickNode(int mx, int my) {                                   ┐
 54      PickObject  pickObject = new PickObject(canvas, scene);.............(7) │
 55      SceneGraphPath sgPath =                                                 │
           pickObject.pickClosest(mx, my, PickObject.USE_GEOMETRY);..........(8) ├(6)
 56      if (sgPath == null) return null;                                        │
 57      Node node = pickObject.pickNode(sgPath, PickObject.PRIMITIVE, 1);)...(9)│
 58      return node;........................................................(10)│
 59    }                                                                         ┘

(1)で java.awt.event.MouseAdapter を継承した内部クラスを定義し、addMouseListener() メソッドで Canvas3D に設定しています。(2)が mouseClicked() メソッドの定義です。(3)では、クリックされた位置のマウス座標を渡して、private メソッドである pickNode() を実行しています。戻り値はピックされた Node です。(4)で getUserData() を使って取得したユーザー定義データを、(5)でコンソール表示しています。

(6)が pickNode() メソッドの定義です。(7)で PickObject を生成しています。(8)でpickClosest() メソッドで、クリック位置に最も近い Node が格納された SceneGraphPath を取得しています。

(9)で SceneGraphPath に格納されている最初の Primitive を取得し、(10)で戻り値として返しています。

このサンプルの実行結果は次のようになります。

PickSelectionFeedback.gif

イベントリスナーを使ったイベント処理も可能ですが、 なるべくBehavior を使うことをおすすめします。 Java 3D の描画スケジューリングの管理外に、 別のイベント処理が存在するのは良い設計ではありません。

■Behaviorを書く

今までは、すでに提供されている Behaviar を使って来ました。自分で Behaviar を継承したクラスを書くにはどうしたら良いでしょうか?

Behavior を継承したクラスでは、次のことを行う必要があります。

  1. initialize() メソッドの最後で、起動条件 (javax.media.j3d.WakeupCondition) を指定して wakeupOn() メソッドを実行する。
    これで、最初のイベントが発生したときに processStimulus() メソッドが実行されるようになる。
  2. processStimulus() メソッドの最後で、次回の起動条件 (WakeupCondition) を指定して wakeupOn() メソッドを実行する。
    ここで initialize()のときとは違う起動条件 (WakeupCondition) を指定することもできる。
    また、wakeupOn() を実行せずに、イベントを 1回で終らせることも出来る。

では、最小限の Behavior を書いてみましょう。指定のミリ秒が経過するたびにコンソールにメッセージを表示するテスト用のサンプルです。

ここでは、起動条件として、javax.media.j3d.WakeupOnElapsedTime を使用しました。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.WakeupCondition
        |
        +--javax.media.j3d.WakeupCriterion
              |
              +--javax.media.j3d.WakeupOnElapsedTime

クラス宣言

public final class WakeupOnElapsedTime
extends WakeupCriterion

コンストラクター

public WakeupOnElapsedTime(long milliseconds) // 起動までのミリ秒

ソースコードは次の通りです。

TimerBehavior.java
 1  // Java 3Dテスト用プログラム
 2  // TimerBehavior.java
 3  //   Copyright (c) 1999 ENDO Yasuyuki
 4  //                      mailto:yasuyuki@javaopen.org
 5  //                      http://www.javaopen.org/j3dbook/index.html
 6  
 7  import java.util.Enumeration;
 8  import javax.media.j3d.Behavior;
 9  import javax.media.j3d.WakeupOnElapsedTime;
10  
11  public class TimerBehavior extends Behavior {
12    protected WakeupOnElapsedTime wup = null; // 起動条件になる WakeupCriterion
13  
14    public TimerBehavior(long sleep) {
15      super();
16      wup = new WakeupOnElapsedTime(sleep); // ミリ秒を指定して起動条件を生成 ...(1)
17    }
18  
19    public void initialize() {                                   ┐
20      System.out.println("initialize()."); //DEBUG               ├(2)
21      wakeupOn(wup);                       // 起動条件の設定 ...(3) │
22    }                                                            ┘
23  
24    public void processStimulus(Enumeration criteria) {               ┐
25      System.out.println("processStimulus()."); //DEBUG               ├(4)
26      wakeupOn(wup);                            // 次回の起動条件 ...(5) │
27    }                                                                 ┘
28  }

コンストラクター(1)では、引数で取得したミリ秒を使って WakeupOnElapsedTime を生成しています。

(2)が initialize() メソッドの定義です。(3)で wakeupOn() メソッドを実行しています。引数は WakeupOnElapsedTime です。これで、指定のミリ秒が経過した後に processStimulus() メソッドが実行されるようになります。

(4)が processStimulus() メソッドの定義です。(5)で wakeupOn() メソッドを実行しています。引数は WakeupOnElapsedTime です。これで、指定のミリ秒が経過した後に、ふたたび processStimulus() メソッドが実行されます。

TimerBehavior の使用方法は次の通りです。

  1. コンストラクターの引数にミリ秒を指定して TimerBehavior を生成する
  2. setSchedulingBounds() メソッドでスケジューリングが有効になる領域 (Bounds) を設定する
  3. BranchGroupaddChild() する

実際のコード例を次に示します。

    TimerBehavior timer = new TimerBehavior(1000); // 1秒後に起動
    timer.setSchedulingBounds(new BoundingSphere(new Point3d(), 100.0));
    root.addChild(timer);

今までのどのサンプルでもかまいません。 TimerBehavior を追加してみてください。実行結果は次のようになります。

$ java TimerBehaviorTest
processStimulus().
processStimulus().
processStimulus().
processStimulus().
processStimulus().
processStimulus().
processStimulus().

initialisze() での初期の起動条件の指定や、processStimulus() での次回以降の起動条件の指定はわかりましたが、このままでは何の役にも立たないので、processStimulus() の中で起動するメソッドを定義した interface を書いてみます。

TimerBehaviorCallback.java
1 // Java 3Dテスト用プログラム
2 // TimerBehaviorCallback.java
3 //   Copyright (c) 1999 ENDO Yasuyuki
4 //                      mailto:yasuyuki@javaopen.org
5 //                      http://www.javaopen.org/j3dbook/index.html
6 
7 public interface TimerBehaviorCallback {
8   void wakeup();
9 }

TimerBehavior のコンストラクターでこの interfaceimpluments したクラスを指定し、processStimulus() メソッドが実行されたときに TimerBehaviorCallback#wakeup() を実行してやることにします。

TimerBehavior.java
 1  // Java 3Dテスト用プログラム
 2  // TimerBehavior.java
 3  //   Copyright (c) 1999 ENDO Yasuyuki
 4  //                      mailto:yasuyuki@javaopen.org
 5  //                      http://www.javaopen.org/j3dbook/index.html
 6  
 7  import java.util.Enumeration;
 8  import javax.media.j3d.Behavior;
 9  import javax.media.j3d.WakeupOnElapsedTime;
10  
11  public class TimerBehavior extends Behavior {
12    protected WakeupOnElapsedTime wup = null;        // 起動条件になる WakeupCriterion
13    protected TimerBehaviorCallback callback = null; // 起動時に呼ぶオブジェクト
14  
15    public TimerBehavior(long sleep, TimerBehaviorCallback newCallback) {
16      super();
17      wup = new WakeupOnElapsedTime(sleep); // ミリ秒を指定して起動条件を生成
18      callback = newCallback;               // 起動時に呼ぶオブジェクトの登録
19    }
20  
21    public void initialize() {
22      System.out.println("initialize()."); //DEBUG
23      wakeupOn(wup);                       // 起動条件の設定
24    }
25  
26    public void processStimulus(Enumeration criteria) {
27      //System.out.println("processStimulus()."); //DEBUG
28  
29      if (callback != null) callback.wakeup();    // 外部オブジェクトを呼ぶ
30  
31      wakeupOn(wup);                              // 次回の起動条件
32    }
33  }

TimerBehaviorCallbackimplements したクラスを書いて、 wakeup() メソッドで起動時の処理を書けば、 一定の間隔で動作する処理を実現できます。

    TimerBehavior timer =
      new TimerBehavior( 1000, new TimerBehaviorCallback() {
        public void wakeup() {
	  // 起動されたときにやりたい処理
	}
      });
    timer.setSchedulingBounds(new BoundingSphere(new Point3d(), 100.0));
    root.addChild(timer);

WakeupCondition のいろいろ

これまで WakeupOnElapsedTime を使って簡単な Behavior を書いてみました。
ここでは Behavior の起動条件を宣言するためのクラスである WakeupCondition について少し詳しく見てみたいと思います。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.WakeupCondition       // 起動条件基底クラス
        |
        +--javax.media.j3d.WakeupAnd       // 起動条件配列のAND
        |
        +--javax.media.j3d.WakeupAndOfOrs  // (起動条件配列のOR)配列のAND
        |
        +--javax.media.j3d.WakeupOr        // 起動条件配列のOR
        |
        +--javax.media.j3d.WakeupOrOfAnds  // (起動条件配列のAND)配列のOR
        |
        +--javax.media.j3d.WakeupCriterion
              |
              +--javax.media.j3d.WakeupOnActivation        // Behavior がアクティブ化された
              |
              +--javax.media.j3d.WakeupOnAWTEvent          // AWT イベントが発生した
              |
              +--javax.media.j3d.WakeupOnBehaviorPost      // 他のBehaviorから起動された
              |
              +--javax.media.j3d.WakeupOnCollisionEntry    // 物体が他の物体に衝突した
              |
              +--javax.media.j3d.WakeupOnCollisionExit     // 物体が他の物体と衝突しなくなった
              |
              +--javax.media.j3d.WakeupOnCollisionMovement // 物体が他の物体と衝突したまま移動した
              |
              +--javax.media.j3d.WakeupOnDeactivation      // Behaviorが非アクティブ化された
              |
              +--javax.media.j3d.WakeupOnElapsedFrame      // 指定のFrameが経過した
              |
              +--javax.media.j3d.WakeupOnElapsedTime       // 指定のミリ秒が経過した
              |
              +--javax.media.j3d.WakeupOnSensorEntry       // Sensorの中心が指定領域に入った
              |
              +--javax.media.j3d.WakeupOnSensorExit        // Sensorの中心が指定領域から外れた
              |
              +--javax.media.j3d.WakeupOnTransformChange   // 指定の TransformGroup が変更された
              |
              +--javax.media.j3d.WakeupOnViewPlatformEntry // ViewPlatformの中心が指定領域に入った
              |
              +--javax.media.j3d.WakeupOnViewPlatformExit  // ViewPlatformの中心が指定領域から外れた

クラス宣言

public abstract class WakeupCriterion
extends WakeupCondition

実際の起動条件となるのは WakeupCriterion のサブクラスです。はじめのサンプルで使った WakeupOnElapsedTime や、マウス、キーボードなどのイベント処理を行う WakeupOnAWTEvent、衝突判定を行う WakeupOnCollisionEntry, WakeupOnCollisionExit, WakeupOnCollisionMovement など様々なクラスがあります。

そして、これらの起動条件の配列 (WakeupCriterion[]) の AND, OR、またはそれらの混合を得るための WakeupAnd, WakeupAndOfOrs, WakeupOr, WakeupOrOfAnds など WakeupCondition の直接のサブクラスもあります。

これらの起動条件を組み合わせていろいろな Behavior を書くことができます。

つぎの項目では WakeupOnAWTEvent を使ってキーボードをイベント処理する簡単なサンプルを書いてみます。

■キーボードイベント処理を書いてみる

WakeupOnAWTEvent を使ったキーボード処理を書いてみます。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.WakeupCondition
        |
        +--javax.media.j3d.WakeupCriterion
              |
              +--javax.media.j3d.WakeupOnAWTEvent

クラス宣言

public final class WakeupOnAWTEvent
extends WakeupCriterion

コンストラクター

public WakeupOnAWTEvent(int AWTId)      // java.awt.Event クラスのイベント定数
public WakeupOnAWTEvent(long eventMask) // java.awt.AWTEvent クラスのイベントマスク定数

WakeupOnAWTEvent のコンストラクターでは起動条件となる java.awt.Event クラスのイベント定数、または java.awt.AWTEvent クラスのイベントマスク定数を指定します。

 27      kpress = new WakeupOnAWTEvent(KeyEvent.KEY_PRESSED);

ここでは java.awt.KeyEvent.KEY_PRESSED イベント定数を指定しています。もし複数のAWTイベントを処理する必要がある場合は次のようにします。

  1. WakuepOnAWTEvent の配列を用意する
  2. それぞれ別のイベント定数を指定して WakeupOnAWTEvent オブジェクトを生成する
  3. 生成した WakeupOnAWTEventWakeupOnAWTEvent 配列の各要素を初期化する
  4. 生成した WakuepOnAWTEvent 配列をコンストラクター引数に指定して WakeupAndWakeupOr を生成する
  5. 生成した WakeupAnd/WakeupOr を引数にして、initialize() メソッドの最後で wakeupOn() を実行する

Behaviorinitialize() では起動条件となる WakeupOnAWTEvent を指定して wakuepOn() を実行します。

 28      wakeupOn(kpress);

指定されたイベントが発生すると、Java 3D は Behavior オブジェクトの processStimulus() を実行します。

processStimulus() では、引数に渡って来た java.util.Enumeration から WakeupCondition を取り出します。

 31    public void processStimulus(Enumeration criteria) {
 32      WakeupOnAWTEvent wevent = null;
 33      AWTEvent[] events = null;
 34      while (criteria.hasMoreElements()) {
 35        wevent = (WakeupOnAWTEvent)criteria.nextElement();
           :
           :
 78      }

ここでは nextElement()取り出した WakeupConditioninitialize() で指定した WakeupOnAWTEvent にキャストして変数 wevent に代入しています。

実際の java.awt.AWTEventWakeupOnAWTEvent#getAWTEvent() メソッドで配列として取り出します。

 36        events = wevent.getAWTEvent();

取り出した AWTEvent を一つづつ処理します。

 37        for (int i=0; i<events.length; i++) {
 38          if (events[i] instanceof KeyEvent) {
                 :
                 :
 75            }
 76          }

ここではキーイベントを仮想キーコードで処理しています。

 39            int code = ((KeyEvent)events[i]).getKeyCode();
 40            switch (code) {
 41            case KeyEvent.VK_UP:
                  :
 47              break;
 48            case KeyEvent.VK_DOWN:
                  :
 54              break;
 55            case KeyEvent.VK_RIGHT:
                  :
 64              break;
 65            case KeyEvent.VK_LEFT:
                  :
 74              break;
 75            }

ここでは KEY_PRESS イベントしか処理していません。Sun のユーティリティー・パッケージ com.sun.j3d.utils.behaviors.keyboard.KeyNavigatorBehavior では KEY_PRESSED, KEY_RELEASED を処理しているので、複数のキーイベントを処理する参考になると思います。

キーイベント処理サンプルのソースは次の通りです。

SimpleKeyBehavior.java
 1  // Java 3Dテストプログラム
 2  // SimpleKeyBehavior.java
 3  //   Copyright (c) 1999 ENDO Yasuyuki
 4  //                      mailto:yasuyuki@javaopen.org
 5  //                      http://www.javaopen.org/j3dbook/index.html
 6  
 7  import java.awt.event.*;
 8  import java.awt.AWTEvent;
 9  import java.util.*;
10  import javax.media.j3d.*;
11  import javax.vecmath.*;
12  
13  public class SimpleKeyBehavior extends Behavior {
14    private static final double ANGLE = Math.PI / 180.0;
15    private static final double STEP = 0.1;
16    private Transform3D t3d = new Transform3D();
17    private Transform3D ct3d = new Transform3D();
18    private Matrix4d matrix = new Matrix4d();
19    private TransformGroup trans = null;
20    private WakeupOnAWTEvent kpress = null;
21  
22    public SimpleKeyBehavior(TransformGroup aTrans) {
23      trans = aTrans;
24    }
25  
26    public void initialize() {
27      kpress = new WakeupOnAWTEvent(KeyEvent.KEY_PRESSED);
28      wakeupOn(kpress);
29    }
30  
31    public void processStimulus(Enumeration criteria) {
32      WakeupOnAWTEvent wevent = null;
33      AWTEvent[] events = null;
34      while (criteria.hasMoreElements()) {
35        wevent = (WakeupOnAWTEvent)criteria.nextElement();
36        events = wevent.getAWTEvent();
37        for (int i=0; i<events.length; i++) {
38          if (events[i] instanceof KeyEvent) {
39            int code = ((KeyEvent)events[i]).getKeyCode();
40            switch (code) {
41            case KeyEvent.VK_UP:
42              //System.out.println("VK_UP");//DEBUG
43              t3d.set(new Vector3d(0.0, 0.0, -STEP));
44              trans.getTransform(ct3d);
45              ct3d.mul(t3d);
46              trans.setTransform(ct3d);
47              break;
48            case KeyEvent.VK_DOWN:
49              //System.out.println("VK_DOWN");//DEBUG
50              t3d.set(new Vector3d(0.0, 0.0, STEP));
51              trans.getTransform(ct3d);
52              ct3d.mul(t3d);
53              trans.setTransform(ct3d);
54              break;
55            case KeyEvent.VK_RIGHT:
56              //System.out.println("VK_RIGHT");//DEBUG
57              t3d.rotY(-ANGLE);
58              trans.getTransform(ct3d);
59              ct3d.get(matrix);
60              ct3d.setTranslation(new Vector3d(0.0, 0.0, 0.0));
61              ct3d.mul(t3d);
62              ct3d.setTranslation(new Vector3d(matrix.m03, matrix.m13, matrix.m23));
63              trans.setTransform(ct3d);
64              break;
65            case KeyEvent.VK_LEFT:
66              //System.out.println("VK_LEFT");//DEBUG
67              t3d.rotY(ANGLE);
68              trans.getTransform(ct3d);
69              ct3d.get(matrix);
70              ct3d.setTranslation(new Vector3d(0.0, 0.0, 0.0));
71              ct3d.mul(t3d);
72              ct3d.setTranslation(new Vector3d(matrix.m03, matrix.m13, matrix.m23));
73              trans.setTransform(ct3d);
74              break;
75            }
76          }
77        }
78      }
79      wakeupOn(kpress);
80    }
81  
82    public Node cloneNodeComponent(boolean forceDuplication) {
83       SimpleKeyBehavior simpleKeyBehavior = new SimpleKeyBehavior(trans);
84       simpleKeyBehavior.duplicateNode(this, forceDuplication);
85       return simpleKeyBehavior;
86    }
87  
88    public void duplicateNode(Node node, boolean forceDuplication) {
89      super.duplicateNode(node, forceDuplication);
90    }
91  
92    public void updateNodeReference(NodeReferenceTable table) {
93      super.updateNodeReferences(table);
94      TransformGroup newTransformGroup = 
95        (TransformGroup)table.getNewObjectReference(trans);
96      trans = newTransformGroup;
97    }
98  }
99  

cloneNodeComponent(), duplicateNode(), updateNodeReference()の3つのメソッドは、cloneTree()を正しく処理するために必要です。

キーボードの矢印キー [↑][↓]で前進/後退、[←][→]で左方向、右方向への回転を行っています。

■衝突判定

衝突判定には次の WakuepCriterion (のサブクラス) を使用します。

javax.media.j3d.WakeupOnCollisionEntry    // 物体が他の物体に衝突した
javax.media.j3d.WakeupOnCollisionExit     // 物体が他の物体と衝突しなくなった
javax.media.j3d.WakeupOnCollisionMovement // 物体が他の物体と衝突したまま移動した

注意点としては、 WakeupOnCollisionEntry が発生しても、 必ず WakeupOnCollisionExit が発生するとは限らない、 ということがあります。 物体が高速で移動しているために WakeupOnCollisionExit が発生せずに通過してしまう場合があります。

ここでは WakeupOnCollisionMovement を使って、壁にぶつかった物体を跳ね返す動作をさせる Behavior を書いてみます。

クラス継承

java.lang.Object
  |
  +--javax.media.j3d.WakeupCondition
        |
        +--javax.media.j3d.WakeupCriterion
              |
              +--javax.media.j3d.WakeupOnCollisionMovement

クラス宣言

public final class WakeupOnCollisionMovement
extends WakeupCriterion

コンストラクター

public WakeupOnCollisionMovement(SceneGraphPath armingPath) // 衝突を検知したい SceneGraphPah

public WakeupOnCollisionMovement(SceneGraphPath armingPath, // 衝突を検知したい SceneGraphPath
                                 int speedHint)             // 幾何学形状で検知するか領域で検知するか

public WakeupOnCollisionMovement(Node armingNode)           // 衝突を検知したい Node

public WakeupOnCollisionMovement(Node armingNode,           // 衝突を検知したい Node
                                 int speedHint)             // 幾何学形状で検知するか領域で検知するか

public WakeupOnCollisionMovement(Bounds armingBounds)       // 衝突を検知したい領域

定数フィールド

public static final int USE_GEOMETRY // 幾何学形状で衝突を検知する
public static final int USE_BOUNDS   // 領域で衝突を検知する

コンストラクター引数に指定された SceneGraphPath, Node, Bounds に、別の物体が衝突(交差)しながら移動したときに Behavior が起動されるようになります。

speedHint には 定数 USE_BOUNDS または USE_GEOMETRY を指定します。デフォルトは USE_BOUNDS です。

USE_BOUNDS では領域 (javax.media.j3d.Bounds) を使って衝突判定するため比較的高速な判定が可能ですが、正確な衝突判定は難しくなります。

USE_GEOMETRY を指定すると正確な判定が可能ですが、衝突判定処理のために実行速度がかなり遅くなります。また、物体が速く動く場合には衝突判定が難しくなります。

WakeupOnCollisionMovement には、衝突の検知対象や、衝突相手を得るためのメソッドが用意されています。

メソッド

public SceneGraphPath getArmingPath()     // 衝突検知対象の SceneGraphPath 

public Bounds getArmingBounds()           // 衝突検知対象の領域

public SceneGraphPath getTriggeringPath() // 衝突相手の SceneGraphPath

public Bounds getTriggeringBounds()       // 衝突相手の領域

javax.media.j3d.SceneGraphPath には、格納されている様々なオブジェクトを取得するメソッドが用意されています。

SceneGraphPath のメソッド (一部)
public final Node getObject() // 終端の Node を取得する

サンプルでは衝突相手の物体を取得するために SceneGraphPath#getObject() を使っています。

サンプルの実行結果を見てください。

CollisionTest.gif

前のサンプルで作成した SimpleKeyBehavior で視点側の TransformGroup を移動させています。

視点の前と後ろに ColorCube を追加し、それぞれ前方と後方の衝突を検知させています。ColorCube が "壁" である Text3D に衝突すると、進行方向の反対方向に跳ね返るようにしています。

衝突検知と跳ね返りによる移動は次のようにしました。

  1. "壁"となる Text3D に、その"親"である TransformGroupsetUserData() でセットしておく
  2. 視点側のTransformGroup、視点の前と後ろのColorCube、進行方向ベクトルを指定し、衝突検知用の Behavoir を生成する
  3. 衝突時に衝突相手の SceneGraphPath を取得し、getObject() メソッドで"壁"の Node を取得する
  4. getUserData() で"壁"の TransformGroup を取得する
  5. "壁"の法線ベクトルを視点から見た方向ベクトルに変換する
  6. "視点側の移動ベクトルの逆ベクトルを求める
  7. 視点側の移動ベクトルの逆ベクトルと"壁"の法線ベクトルを使って、"壁"による反射ベクトルを求める
  8. 反射ベクトルを使って視点を移動させる

衝突判定の Behavior のソースは次の通りです。

WallCollisionBehavior.java
  1  // Java 3Dテストプログラム
  2  // WallCollisionBehavior.java
  3  //   Copyright (c) 1999 ENDO Yasuyuki
  4  //                      mailto:yasuyuki@javaopen.org
  5  //                      http://www.javaopen.org/j3dbook/index.html
  6  
  7  import java.util.*;
  8  import javax.media.j3d.*;
  9  import javax.vecmath.*;
 10  
 11  public class WallCollisionBehavior extends Behavior {
 12    public static final int USE_BOUNDS = WakeupOnCollisionMovement.USE_BOUNDS;
 13    public static final int USE_GEOMETRY = WakeupOnCollisionMovement.USE_GEOMETRY;
 14  
 15    private static final Vector3d ORIGIN = new Vector3d(0.0, 0.0, 0.0); // 原点
 16    private static final double VSCALE = 0.1;      // 衝突時の移動量
 17  
 18    private Transform3D ot3d = new Transform3D();  // 衝突を検知したい物体のT3D
 19    private TransformGroup otrans = null;          // 衝突を検知したい物体のTG
 20    private Node onode = null;                     // 衝突を検知したい物体
 21    private Vector3d ovector = null;               // 衝突を検知したい物体の進行方向
 22  
 23    private Transform3D nt3d = new Transform3D();  // 壁のT3D
 24    private Transform3D rt3d = new Transform3D();  // 衝突による移動
 25  
 26    private int hint = USE_BOUNDS;                 // 領域検査か幾何学的検査か
 27    private WakeupOnCollisionMovement move = null; // Behavior起動条件
 28  
 29    public WallCollisionBehavior(TransformGroup trans, Node node, Vector3d vector) {
 30      this(trans, node, vector, USE_BOUNDS);
 31    }
 32  
 33    public WallCollisionBehavior(TransformGroup trans, Node node, Vector3d vector, int hint) {
 34      onode = node;
 35      otrans = trans;
 36      ovector = vector;
 37      hint = hint;
 38    }
 39  
 40    public void initialize() {
 41      move = new WakeupOnCollisionMovement(onode, hint);
 42      wakeupOn(move);
 43    }
 44  
 45    public void processStimulus(Enumeration criteria) {
 46      while (criteria.hasMoreElements()) {
 47        WakeupOnCollisionMovement wakeup =
 48          (WakeupOnCollisionMovement)criteria.nextElement();
 49        SceneGraphPath npath = wakeup.getTriggeringPath(); // 衝突した壁を含むPath
 50        Node node = npath.getObject();
 51        System.out.println("\nnode.getBounds()=" + node.getBounds());//DEBUG
 52        System.out.println("onode.getBounds()=" + onode.getBounds());//DEBUG
 53        TransformGroup ntrans = (TransformGroup)node.getUserData();//壁にセットしてあったTG
 54        if (ntrans != null) {
 55          ntrans.getTransform(nt3d);                        // 壁のT3D
 56          System.out.println("nt3d=\n" + nt3d);//DEBUG
 57          nt3d.setTranslation(ORIGIN);                      // 原点に戻しておく
 58          Vector3d nvec = new Vector3d(0.0, 0.0, 1.0);      // 壁の法線ベクトル初期値
 59          nt3d.transform(nvec);                             // 壁の法線ベクトルを計算
 60          
 61          otrans.getTransform(ot3d);                        // 視点のT3D
 62          System.out.println("o.x=" + ovector.x + ", o.y=" + ovector.y + ", o.z=" + ovector.z);//DEBUG
 63          System.out.println("ot3d=\n" + ot3d);//DEBUG
 64          ot3d.setTranslation(ORIGIN);                      // 原点に戻しておく
 65          ot3d.transpose();                                 // 視点マトリックスの転置
 66          ot3d.transform(nvec);                             // 壁の法線を視点座標に変換
 67          System.out.println("n.x=" + nvec.x + ", n.y=" + nvec.y + ", n.z=" + nvec.z);//DEBUG
 68         
 69          Vector3d onegate = new Vector3d();                // 物体の逆ベクトル
 70          onegate.negate(ovector);                          // 逆ベクトルを計算
 71          double odotn = onegate.dot(nvec);                 // 内積 O・N
 72          System.out.println("odotn=" + odotn);//DEBUG      
 73          nvec.scale(2.0 * odotn);                          // 2(O・N)N
 74          System.out.println("n.x=" + nvec.x + ", n.y=" + nvec.y + ", n.z=" + nvec.z);//DEBUG
 75          Vector3d rvec = new Vector3d(nvec);               // 反射ベクトル
 76          rvec.sub(nvec, onegate);                          // 2(O・N)N - O // Phong の式
 77          rvec.normalize();                                 // 正規化
 78          rvec.scale(VSCALE);                               // 移動量でスカラー倍
 79          System.out.println("r.x=" + rvec.x + ", r.y=" + rvec.y + ", r.z=" + rvec.z);//DEBUG
 80          rt3d.set(rvec);                                   // 移動ベクトルをセットする
 81          
 82          otrans.getTransform(ot3d);                        // もう一度現在のTransform取得
 83          ot3d.mul(rt3d);                                   // 移動を乗算
 84          otrans.setTransform(ot3d);                        // 移動させる
 85        }
 86      }
 87  
 88      wakeupOn(move);
 89    }
 90  
 91    public Node cloneNodeComponent(boolean forceDuplication) {
 92       WallCollisionBehavior wallCollisionBehavior =
 93         new WallCollisionBehavior(otrans, onode, ovector, hint);
 94       wallCollisionBehavior.duplicateNode(this, forceDuplication);
 95       return wallCollisionBehavior;
 96    }
 97  
 98    public void duplicateNode(Node node, boolean forceDuplication) {
 99      super.duplicateNode(node, forceDuplication);
100    }
101  
102    public void updateNodeReference(NodeReferenceTable table) {
103      super.updateNodeReferences(table);
104  
105      Node newNode =
106        (Node)table.getNewObjectReference(onode);
107      onode = newNode;
108  
109      TransformGroup newTransformGroup = 
110        (TransformGroup)table.getNewObjectReference(otrans);
111      otrans = newTransformGroup;
112    }
113  
114  }

サンプル・アプレットのソースは次の通りです。

CollisionTest.java (一部)
 46    private BranchGroup createSceneGraph() {
 47      BranchGroup root = new BranchGroup();
 48  
 49      Bounds bounds = new BoundingSphere(new Point3d(), 100.0);
 50  
 51      // 視点側の TG を取得
 52      TransformGroup vtrans = universe.getViewingPlatform().getViewPlatformTransform();
 53  
 54      // 視点の前に置く物体の root 
 55      PlatformGeometry pg = new PlatformGeometry();
 56      
 57      // キーボードによる視点移動 Behavior
 58      SimpleKeyBehavior kb = new SimpleKeyBehavior(vtrans);
 59      kb.setSchedulingBounds(bounds);
 60      pg.addChild(kb);
 61  
 62      // 環境光源
 63      AmbientLight alight = new AmbientLight();
 64      alight.setInfluencingBounds(bounds);
 65      root.addChild(alight); // どこにaddChild()しても良い
 66  
 67      // 視点に固定する平行光源
 68      DirectionalLight light =
 69        new DirectionalLight( new Color3f(1.0f, 1.0f, 1.0f),
 70                              new Vector3f(0.57f, -0.57f, -0.57f) );
 71      light.setInfluencingBounds(bounds);
 72      pg.addChild(light);
 73      
 74      // フロントバンパー
 75      Transform3D ft3d = new Transform3D();
 76      ft3d.setTranslation(new Vector3d(0.0, -0.13, -0.5));
 77      TransformGroup ftrans = new TransformGroup(ft3d);
 78      pg.addChild(ftrans);
 79  
 80      ColorCube fcube = new ColorCube(0.04);
 81      fcube.setCapability(ColorCube.ALLOW_BOUNDS_READ);//TEST
 82      ftrans.addChild(fcube);
 83  
 84      // 最小限の BoundingBox
 85      BoundingBox box = new BoundingBox( new Point3d(-0.04, -0.04, -0.04),
 86                                         new Point3d( 0.04, 0.04, 0.04) );
 87  
 88      WallCollisionBehavior fcollision =
 89        new WallCollisionBehavior(vtrans, fcube, new Vector3d(0.0, 0.0, -1.0)); // 前向き
 90      fcollision.setSchedulingBounds(box);
 91      root.addChild(fcollision);
 92  
 93      // リヤバンパー
 94      Transform3D rt3d = new Transform3D();
 95      rt3d.setTranslation(new Vector3d(0.0, -0.13, 0.5));
 96      TransformGroup rtrans = new TransformGroup(rt3d);
 97      pg.addChild(rtrans);
 98  
 99      ColorCube rcube = new ColorCube(0.04);
100      rcube.setCapability(ColorCube.ALLOW_BOUNDS_READ);//TEST
101      rtrans.addChild(rcube);
102      
103      WallCollisionBehavior rcollision =
104        new WallCollisionBehavior(vtrans, rcube, new Vector3d(0.0, 0.0, 1.0)); // 後ろ向き
105      rcollision.setSchedulingBounds(box);
106      root.addChild(rcollision);
107  
108      universe.getViewingPlatform().setPlatformGeometry(pg);
109  
110      // 衝突相手となる壁
111      root.addChild(createText3D( "Front", 0.0, -0.5, -3.0, 0.0,
112                                  0.0f, 1.0f, 0.0f ));                      // green
113  
114      root.addChild(createText3D( "Back", 0.0, -0.5, 3.0, Math.PI,
115                                  0.0f, 0.0f, 1.0f ));                      // blue
116  
117      root.addChild(createText3D( "Left", -3.0, -0.5, 0.0, (Math.PI / 2.0),
118                                  1.0f, 1.0f, 0.0f ));                      // yellow
119  
120      root.addChild(createText3D( "Right", 3.0, -0.5, 0.0, -(Math.PI / 2.0),
121                                  0.0f, 1.0f, 1.0f ));                      // cyan
122  
123      // チェッカー模様の床 (衝突判定の対象外)
124      root.addChild(createFloor());
125  
126      return root;
127    }

"壁"となる Text3D の生成はつぎのように行っています。

129    private TransformGroup createText3D( String text,
130                                         double x, double y, double z, double angle,
131                                         float r, float g, float b)
132    {
133      Transform3D t3d = new Transform3D();
134      t3d.setTranslation(new Vector3d(x, y, z));
135      Transform3D rt3d = new Transform3D();
136      rt3d.rotY(angle);
137      t3d.mul(rt3d);
138      TransformGroup trans = new TransformGroup(t3d);
139      trans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
140  
141      Text3D text3d =
142        new Text3D( new Font3D(new Font("dialog", Font.PLAIN, 1), new FontExtrusion() ),
143                    text, new Point3f(), Text3D.ALIGN_CENTER, Text3D.PATH_RIGHT );
144  
145      Material mat = new Material();
146      mat.setDiffuseColor(new Color3f(r, g, b));
147      mat.setShininess(128);
148      Appearance app = new Appearance();
149      app.setMaterial(mat);
150      
151      Shape3D shape = new Shape3D(text3d, app);
152      shape.setCapability(Shape3D.ALLOW_BOUNDS_READ);//TEST
153      shape.setUserData(trans);         // ユーザーデータに親のTGをセットしておく
154      trans.addChild(shape);
155  
156      return trans;
157    }

WallCollisionBehavior で衝突相手の TransformGroup を取得できるように、setUserData() で"親"の TransformGroup をセットしています。

このサンプルでは IndexedQuadArray を使ってチェッカー模様の"床"を生成していますが、距離が近いためそのままでは常に床との衝突が判定されてしまいます。

衝突検知の対象から床を除外するため、Node#setCollidable() で床との衝突を検知させないようにしています。

159    // チェッカー模様の床を作る
160    private BranchGroup createFloor() {
161      BranchGroup bg = new BranchGroup();
162  
163      int roop = 40;
164  
165      Point3f[] vertices = new Point3f[roop * roop];
166  
167      float start = -20.0f;
168  
169      float x = start;
170      float z = start;
171  
172      float step = 1.0f;
173  
174      int[] indices = new int[(roop - 1)*(roop - 1) * 4];
175      int n = 0;
176  
177      Color3f white = new Color3f(1.0f, 1.0f, 1.0f);
178      Color3f black = new Color3f(0.0f, 0.0f, 0.0f);
179      Color3f[] colors = { white, black };
180  
181      int[] colorindices = new int[indices.length];
182  
183      for (int i=0; i<roop; i++) {
184        for (int j=0; j<roop; j++) {
185          vertices[i*roop + j] = new Point3f(x, -1.0f, z); 
186          z += step;
187          if (i<(roop - 1) && j<(roop - 1)) {
188            int cindex = (i % 2 + j) % 2;
189            colorindices[n] = cindex; indices[n++] = i       * roop + j;
190            colorindices[n] = cindex; indices[n++] = i       * roop + (j + 1);
191            colorindices[n] = cindex; indices[n++] = (i + 1) * roop + (j + 1);
192            colorindices[n] = cindex; indices[n++] = (i + 1) * roop + j;
193          }
194        }
195        z = start;
196        x += step;
197      }
198  
199      IndexedQuadArray geom = new IndexedQuadArray( vertices.length,
200                                                    GeometryArray.COORDINATES |
201                                                    GeometryArray.COLOR_3,
202                                                    indices.length );
203      geom.setCoordinates(0, vertices);
204      geom.setCoordinateIndices(0, indices);
205      geom.setColors(0, colors);
206      geom.setColorIndices(0, colorindices);
207  
208      Shape3D floor = new Shape3D(geom);
209      floor.setCollidable(false);        // 衝突を検知させないようにする
210      bg.addChild(floor);
211  
212      bg.compile();
213  
214      return bg;
215    }

このサンプルでは USE_BOUNDS で衝突検知を行っています。こ の場合あまり正確な衝突判定はできません。 USE_GEOMETRY を使うと正確な衝突判定ができますが、 速い移動のときに連続して衝突検知することができない場合がありました。

迷路を使ったアクションゲームのような衝突判定は、 このサンプルの方法では実現できません。 迷路を使ったアクションゲームでは、 迷路の壁の平面で空間を細かく分割し、 その各空間ごとに衝突判定を行っています。

ENDO Yasuyuki