Java3D Topics

このページには、私自身が Java3D を使ってプログラムを組んでいく過程で出会ったいろいろなトピックスについて書いてあります。解決されているものもあれば、分からないままのものもあります。原則として、ちょっと本やインターネットで調べればわかるようなことは書いてありません。内容は、できるだけ平易に、かつ具体的に書きたいと思います。

[戻る]


[2000.1.20] マウスでクリックされた立体表面の座標値を得るには

Java3D では、Pick と呼ばれる機能を使用して、マウスクリックされた3次元オブジェクトを特定することができます。この機能を利用して、PickTranslateBehavior などの com.sun.j3d.utils.behaviors.picking パッケージ内のビヘイビアが実装されています。

しかし、これらのビヘイビアでは、クリックされた立体を移動、回転、拡大縮小するのに適当な値を足したり掛けたりしており、マウスの移動に追従した立体の移動を実現していません。実際に PickTranslateBehavior を使って立方体を移動させると、移動位置がマウスポインタと異なるために、操作上、かなりの違和感があります。

そこで、ここでは、マウスに追従した移動を実現するための第一歩として、マウスによってクリックされた立体の表面上の座標を得る方法を説明します。

処理の大まかな流れは以下の通りです。

  1. Canvas3D と BranchGroup から PickObject を作成します。
  2. PcikObject の generatePickRay() を使って、マウス座標 (x,y) から PickRay を得ます。
  3. BranchGroup の pickClosest() を使って、PickRay から SceneGraphPath を得ます。
  4. SceneGraphPath の getObject() でパスの最下位の Shape3D を取り出し、intersect() メソッドから交差点までの距離 dist を得ます。
  5. PickRay の始点と方向ベクトルと dist から、交点の座標が得られます。

プログラムコードは以下の通りです。

  /**
    ピックされた座標を得る
    */
  public Point3d getPickedCoordinate(Canvas3D c3d,BranchGroup bg,int x,int y){
    PickObject po=new PickObject(c3d,bg);
    PickRay pr=(PickRay)po.generatePickRay(x,y);
    SceneGraphPath sgp=bg.pickClosest(pr);
    if(sgp!=null){
      double[] dist=new double[1];
      Shape3D shape=(Shape3D)sgp.getObject();
      if(shape instanceof Shape3D){
	shape.intersect(sgp,pr,dist);
	Point3d org=new Point3d();
	Vector3d dir=new Vector3d();
	pr.get(org,dir);
	dir.scaleAdd(dist[0],org);
	org.set(dir);
	return org;
      }
    }
    return null;
  }

視線ベクトルと距離から、交点が出るというのがミソですね。

注意する点として、交差判定をを行うためには、対象となる Geometry に Geometry.ALLOW_INTERSECT フラグがセットされていなければなりません。たとえば、ColorCube のクリック座標を得るためには、あらかじめ、

ColorCube cc=new ColorCube(0.2);
cc.getGeometry().setCapability(Geometry.ALLOW_INTERSECT);
tg.addChild(cc);

のように、フラグを立てておきます。また、クリックされた立体が Shape3D でない場合は座標を得ることはできません。(Shape3D 以外の場合として Morph の場合がありますが未対応です)

それから、ここで得られる座標値は(おそらく)最上位座標系における値です。つまり、クリックされた立体が持つ座標系とは異なっていると考えられます。したがって、最初に書いた、マウスに追従して立体を移動させたいような場合は、さらに、何らかの座標変換を施す必要があると思います。これについては、別な項で書く予定です。

[ TOP ]


[2000.1.21] マウスでクリックされた立体表面の座標値を得るには(改良版)

マウスでクリックされた立体の表面座標を得る方法を [2000.1.20] に書きましたが、この中で使っている pickClosest() メソッドのソースを読んだら何ともタコなことをしているので、それなら自分でやったほうが効率がいいに決まってるということで、改良版をアップします。

その前に、どうしてタコなのかを簡単に説明しましょう。

PickObject クラスの pickClosest() メソッドは、USE_GEOMETRY フラグで使用した場合、最初にすべてのノードリストを SceneGraphPath から求めて、それらの中の Shape3D か Morph のインスタンスについて intersect() を調べて距離を記憶して、それらをすべてクイックソートで並べ替えてから一番目のデータを返しています。

しかし、これはソートされたデータすべてが必要ならば仕方ありませんが、一番目のデータだけがほしいのならば、何もソートまですることはありません。クイックソートアルゴリズムがいくら高速だからといって、しないで済むものをわざわざすることはないでしょう。

それに、[2000.1.20] の場合、求められた Shape3D についてもう一度 intersect を計算しているわけですから、無駄の重複です。

そういうわけで、一回の処理で必要な情報が全部そろうように作り変えてみました。もっとも、コアAPI のソースは公開されていませんから、BranchGroup クラスの pickAll() メソッドがどうなっているかまではわかりません。同様なタココードでないことを祈ります。

以下に、改良したマウスピック用クラス MyPick を示します。

/*****************************************
  MyPick
  ---
  効率よい Pick クラス。
  画面をマウスでクリックしたときに、
  どの Shape3D,Morph のどの座標をクリックしたかを得ます。
  *****************************************/
import javax.media.j3d.*;
import javax.vecmath.*;

public class MyPick {

  // 定数

  public static final int NON=0;	// 何もピックされない
  public static final int SHAPE=1;	// 形状がピックされた
  public static final int MORPH=2;	// モーフィングがピックされた

  // 変数

  public int code;			// ピック結果コード
  public SceneGraphPath sgp;		// その立体のシーングラフパス
  public PickRay ray;			// 視線
  public Shape3D shape;			// 形状
  public Morph morph;			// モーフィング
  public Point3d eye_pt;		// 視点座標
  public Point3d mouse_pt;		// マウス座標
  public Vector3d view_vec;		// 視線ベクトル
  public double dist;			// 距離
  public Point3d picked_pt;		// ピックされた座標
  Transform3D motion;			// イメージプレートからビューへの変換
  double[] d;				// 仮の距離

  /**
    生成
    */
  public MyPick(){
    eye_pt=new Point3d();
    mouse_pt=new Point3d();
    view_vec=new Vector3d();
    picked_pt=new Point3d();
    motion=new Transform3D();
    d=new double[1];
  }

  /**
    ピック
    ピック結果のコードを返します。
    */
  public int pick(Canvas3D c3d,BranchGroup bg,int x,int y){

    code=NON;

    /*** 視線の生成 ***/

    // イメージプレート上の視点座標を得ます
    c3d.getCenterEyeInImagePlate(eye_pt);

    // イメージプレート上のマウス座標を得ます
    c3d.getPixelLocationInImagePlate(x,y,mouse_pt);

    // 平行投影の場合は、視点をマウス座標にあわせます
    // (Z座標だけが異なります)
    if(c3d.getView().getProjectionPolicy()==View.PARALLEL_PROJECTION){
      eye_pt.x=mouse_pt.x;
      eye_pt.y=mouse_pt.y;
    }

    // イメージプレートから仮想世界への変換を得ます
    c3d.getImagePlateToVworld(motion);

    // 各点をビュー世界座標に変換します
    motion.transform(eye_pt);
    motion.transform(mouse_pt);
    view_vec.sub(mouse_pt,eye_pt);
    view_vec.normalize();
      
    // 視線を生成します
    ray=new PickRay(eye_pt,view_vec);

    /*** ピックした立体の情報を得る ***/

    // シーングラフパスを得ます
    SceneGraphPath[] sgp_arr=null;
    if((sgp_arr=bg.pickAll(ray))==null)
      return code;

    // すべてのパスの末尾の Shape3D か Morph について
    // 交差判定を行い、最も近いものを探します。
    Node min_node=null;
    double min_dist=Double.MAX_VALUE;
    SceneGraphPath min_sgp=null;
    for(int i=0;i<sgp_arr.length;i++){
      Node node=sgp_arr[i].getObject();
      boolean found=false;
      if(node instanceof Shape3D)
	found=((Shape3D)node).intersect(sgp_arr[i],ray,d);
      else if(node instanceof Morph)
	found=((Morph)node).intersect(sgp_arr[i],ray,d);
      if(found && min_dist>d[0]){
	min_node=node;
	min_dist=d[0];
	min_sgp=sgp_arr[i];
      }
    }
    if(min_node==null) return code;
    if(min_node instanceof Shape3D){
      shape=(Shape3D)min_node;
      code=SHAPE;
    }
    else if(min_node instanceof Morph){
      morph=(Morph)min_node;
      code=MORPH;
    }
    dist=min_dist;
    sgp=min_sgp;

    // 距離と視点、視線ベクトルから交点を得ます
    picked_pt.scaleAdd(dist,view_vec,eye_pt);

    return code;
  }
}

視線用の PickRay を計算して作っているのでちょっと長くなっていますが、それでも PickObject クラスの generatePickRay() メソッドも同じことをやるわけですから、処理効率は変わりません。基本的にコアAPI だけを使うようにしました。Canvas3D クラスの getCenterEyeInImagePlate() や getPixelLocationInImagePlate() などを使っています。これにより、同じような処理を何度もやらなくてもいいので、効率アップしている(はず)です。

このクラスのインスタンスを作り、pick() メソッドを呼び出すことで、その座標にある立体の、クリックされた座標を含む情報がインスタンスのメンバに格納されます。また、pick() メソッド自身はクリックされた立体の表面上の座標を返します。

座標はいずれも最上位の仮想ワールド座標です。

[ TOP ]


[2000.1.27] TransformGroup の子(BranchGroup ノード)を detach すると、その後、別な子を addChild() する際に NullPointerException が発生

TG を子操作可能な状態にして、いくつかの子を追加します。次に、マウスイベント等によってその子のれかを取り外します。すると、その後は、TG に子を追加しようとすると NullPointerException が発生して不可能になります。numChildren() メソッドで子の数をチェックすると、きちんと減っていますが、どうも、内部的に null になって残っているのではないかと邪推しています。

時間がないので、サンプルコードは機会を見て示すつもりです。

また、現象自体の正確性もいまいち自信がないので、これもそのうちに・・・。

[ TOP ]


[2000.2.1] GeometryArray のサブクラスがピックできないことへの対応

Java3D 1.1.3 では(それ以前でもですが)、GeometryArray のサブクラス形状がピックできません。これは、[Java3Djp:00450]、[Java3Djp:00781] あたりに報告されているバグですが、ピックできないとあきらめてしまっていては仕事が進まないので、ピックできるように Shape3D クラスを拡張しました。

作成したクラスは、Shape3D の派生クラスで IntersectableShape3D です。このクラスは、ここにそのまま乗せるのはちょっと長いので、見たい方はリンクをたどってください。使い方は簡単で、GeometryArray のサブクラスをピックしたい場合は、そのジオメトリをくっつける形状として、Shape3D ではなくて IntersectableShape3D を使うだけです。

IntersectableShape3D は、親クラスの Shape3D の機能に加えて、ピックされたジオメトリの位置に応じたコードが得られるという利点があります。例えば2本の線分からなる LineArray をピックした場合を説明します。3本の線分を L1,L2 とし、それぞれの始点終点を SP1,EP1,SP2,EP2 とすると、ピックは点→線分の順に行われます。そして、SP1 → EP1 → SP2 → EP2 → L1 → L2 の順番にそれぞれ 0,1,2,3,4,5 のコードが割り当てあられます。ピック後にその形状の getCode() メソッドは、どこがピックされたかを表すコードを返します。

きわめて私的な理由から(自分が使わないという理由で)、LineArray と IndexedLineArray 以外は処理してありません。また、他の GeometryArray がピックできないのかどうかも確認していません。でも、他の GeometryArray についても同じ方法でピックできますから、必要な方はソースを読んで試しましょう。(Point 系については例外※

以下に、どのようにしてピックを実現したかを簡単に説明します。

Java3D 1.1.3 で GeometryArray のサブクラスのオブジェクトのピックに失敗するのは、そのジオメトリが、自分を含む Shape3D の intersect() メソッドに反応しないことが原因です。より詳しく言えば、ジオメトリをピックする際には、まずその境界と交差するかどうかを調べるために BranchGroup クラスの pickAll() というメソッドが呼び出されます。その後、さらに USE_GEOMETRY モードでピックする場合には、各形状ごとに Shape3D.intersect() を実行します。GeometryArray を含む形状は、BranchGroup.pickAll() には反応する(※一部例外があるようです)のですが、その後の Shape3D.intersect() において、ALLOW_INTERSECT キャパビリティを設定していても、交差判定が正しく行われずに、結果としてピックに失敗してしまうわけです。

※ 一部例外とは PointArray 系のサブクラスです。これは BranchGroup.pickAll() にすら反応しません。よって、点をピックする場合は、ここで述べている方法では対応できません。ただし、線の端としての点はピックできます。

なぜ intersect() に失敗するのか(そして、どうしてこの程度のバグがいつまでたっても取れないのか)は不明ですが、対応策としては、Shape3D の intersect() メソッドをオーバーライドして、その形状のジオメトリが GeometryArray のサブクラスであった場合、独自に交差判定を行えばよいことになります。

intersect() メソッドはシーングラフパスと視線レイを引数として受け取りますから、交差判定を行うには十分の情報が得られるわけです。

どのように交差判定を行っているかは、ソースコードを参照してください。基本的には、単純なベクトル演算を行っているだけです。

[ TOP ]