Imaginary Code

from kougaku-navi.net

Processingで画面座標に対応する空間座標を計算する

 3Dのアプリケーションにおいて、画面をクリックして空間中のオブジェクトの表示位置を指定するときなど、画面の2次元座標に対応する3次元空間座標が欲しいときがあります。こんなとき、どんな計算を行えば良いかについて解説します。コードはProcessing 2.2.1にて動作を確認。




画面位置に対応するローカル座標

 まず本題に入る前に、3次元空間中の点がウィンドウ上に描画されるまでの過程を理解しておく必要があります。ローカル座標系におかれたCG物体は、モデリング変換、ビューイング変換、投影変換、ビューポート変換を経て画面に描画されます。





 モデリング変換を行う行列をMm、ビューイング変換を行う行列をMv、投影変換を行う行列(プロジェクション行列)をP、ビューポート変換を行う行列をUと置きます。それぞれ4x4の行列です。このうち、モデリング変換とビューイング変換はモデルビュー行列という1つの行列で表現されます。一連の変換過程をまとめて数式で表現すると以下のようになります。



求めたいのはウィンドウ座標系からローカル座標系への変換なので、この式の逆変換を考えます。



 ここで、プロジェクション行列Pとモデルビュー行列Mはプログラム的に取得可能な行列です。ビューポート変換行列Uは取得できない(?)ようなので、数式から計算することにします。ビューポート変換行列Uは以下のようになります(ウィンドウ座標系において左上隅に原点を置いた場合)。




 以上のロジックを踏まえて、ウィンドウ座標系からローカル座標系への座標値の変換を実装した例が以下のコードです。実行すると、マウスカーソル上に黒い点(球)が表示されます。

void setup() {
  size( 400, 300, P3D);
}

void draw() {
  background(200);

  // カーソルの位置に対応する空間座標を計算する
  PVector result = unProject(mouseX, mouseY, -1);
  
  // カーソルの3次元位置に球を表示
  translate( result.x, result.y, result.z );
  sphere(2);
}

// ウィンドウ座標系からローカル座標系への変換(逆投影)を行う関数
PVector unProject(float winX, float winY, float winZ) {
  PMatrix3D mat = getMatrixLocalToWindow();  
  mat.invert();
  
  float[] in = {winX, winY, winZ, 1.0f};
  float[] out = new float[4];
  mat.mult(in, out);  // Do not use PMatrix3D.mult(PVector, PVector)
  
  if (out[3] == 0 ) {
    return null;
  }
  
  PVector result = new PVector(out[0]/out[3], out[1]/out[3], out[2]/out[3]);  
  return result;
}

// ローカル座標系からウィンドウ座標系への変換行列を計算する関数
PMatrix3D getMatrixLocalToWindow() {
  PMatrix3D projection = ((PGraphics3D)g).projection; // プロジェクション行列
  PMatrix3D modelview = ((PGraphics3D)g).modelview;   // モデルビュー行列
  
  // ビューポート変換行列
  PMatrix3D viewport = new PMatrix3D();
  viewport.m00 = viewport.m03 = width/2;
  viewport.m11 = -height/2;
  viewport.m13 =  height/2;

  // ローカル座標系からウィンドウ座標系への変換行列を計算  
  viewport.apply(projection);
  viewport.apply(modelview);
  return viewport;
}

 上記コードにおいて、球(sphere)は2次元的に配置しているのではなく、ウィンドウ座標系で表現されるマウスカーソル座標をローカル座標系に変換してから配置しています。その計算をやっているのがunProject()という関数です。ウィンドウ座標は(winX, winY, winZ)の3つで表現されます。winZは奥行きを表わしており、-1.0にすると視錘台の手前の面(near clipping plane)、1.0にすると視錘台の奥の面(far clipping plane)の位置に対応します。





 ここでやっている計算内容は、OpenGLのgluUnProject()という関数とほぼ同じです(Y軸の向きだけ違う)。ProcessingからgluUnProject()を直接呼ぶほうが賢いかもしれませんが、Processing 2.2.1におけるGLUへのアクセス方法がわからなかったので現状こうしています。OpenGLまわりは、Processingのバージョンによって仕様が異なるのでややこしいんですよね(古いコードが動かなかったり)。

画面座標に対応する3次元平面上の点

 画面をクリックして空間中のオブジェクトの表示位置を指定する方法について考えてみます。画面上の点に対応する3次元空間中の点は、画面上の点と焦点を結ぶ直線上に無数に存在します。そこで、3次元空間内に地面を作り、画面上の点に対応する地面上の点を求めることにします。これを図で描くと以下のようになります。





 ワールド座標系で図示していますが、これをローカル座標系(現在の座標系)と読み替えても同じです。上図のようにパラメータを定義したとき、画面上の点に対応する地面上の点pは次式で与えられます。



 画面上の点に対応する3次元座標wはさきほどのunProject()を使って計算された座標値です。目の位置eはモデルビュー行列の逆行列の4列目の成分から得ることができます。平面上の任意の点fと平面の方向を表わす法線ベクトルnは、床をどう配置したかの情報に基づいて与えてください。

 これをプログラムで実装すると以下のようになります。

void setup() {
  size( 500, 500, P3D);
}

void draw() {
  background(200);

  // 視点の位置を設定
  camera( 1160, -1960, 1730, 890, -1200, 1200, 0, 1, 0 );

  // カーソル位置に対応する床面上の座標を計算
  PVector floorPos = new PVector( 500, 300, 100 ); // 床の座標
  PVector floorDir = new PVector( 0, -1, 0 );      // 床の法線ベクトル
  PVector mousePos = getUnProjectedPointOnFloor( mouseX, mouseY, floorPos, floorDir );
  
  // 床
  pushMatrix();
    translate( floorPos.x, floorPos.y, floorPos.z );
    fill(255);
    box( 2000, 1, 2000 );
  popMatrix();

  // カーソル位置に立方体を描画
  pushMatrix();
    translate( mousePos.x, mousePos.y, mousePos.z );
    fill(255, 0, 0);
    box(200);
  popMatrix();
}

// 画面座標に対応する床面上の座標を計算する関数
PVector getUnProjectedPointOnFloor(float screen_x, float screen_y, PVector floorPosition, PVector floorDirection) {

  PVector f = floorPosition.get(); // 床の位置
  PVector n = floorDirection.get(); // 床の方向(法線ベクトル)
  PVector w = unProject(screen_x, screen_y, -1.0); // 画面上の点に対応する3次元座標
  PVector e = getEyePosition(); // 視点位置

  // 交点の計算  
  f.sub(e);
  w.sub(e);
  w.mult( n.dot(f)/n.dot(w) );
  w.add(e);

  return w;
}

// 現在の座標系における視点の位置を取得する関数
PVector getEyePosition() {
  PMatrix3D mat = (PMatrix3D)getMatrix(); // モデルビュー行列を取得
  mat.invert();
  return new PVector( mat.m03, mat.m13, mat.m23 );
}

// ウィンドウ座標系からローカル座標系への変換(逆投影)を行う関数
PVector unProject(float winX, float winY, float winZ) {
  PMatrix3D mat = getMatrixLocalToWindow();  
  mat.invert();
  
  float[] in = {winX, winY, winZ, 1.0f};
  float[] out = new float[4];
  mat.mult(in, out);  // Do not use PMatrix3D.mult(PVector, PVector)
  
  if (out[3] == 0 ) {
    return null;
  }
  
  PVector result = new PVector(out[0]/out[3], out[1]/out[3], out[2]/out[3]);  
  return result;
}

// ローカル座標系からウィンドウ座標系への変換行列を計算する関数
PMatrix3D getMatrixLocalToWindow() {
  PMatrix3D projection = ((PGraphics3D)g).projection; // プロジェクション行列
  PMatrix3D modelview = ((PGraphics3D)g).modelview;   // モデルビュー行列
  
  // ビューポート変換行列
  PMatrix3D viewport = new PMatrix3D();
  viewport.m00 = viewport.m03 = width/2;
  viewport.m11 = -height/2;
  viewport.m13 =  height/2;

  // ローカル座標系からウィンドウ座標系への変換行列を計算  
  viewport.apply(projection);
  viewport.apply(modelview);
  return viewport;
}


 実行結果は以下のようになります。マウスカーソルの位置に対応する平面上の座標が計算できていることがわかります。

まとめ

 CGが画面に投影されるまでの変換過程を理解していると、数学的にいろいろできますね。いじょ。

追記

 ここで解説したテクニックを使って、忍者くんをクリックした場所に歩かせるサンプルを作りました。こちらもあわせてごらんください。