Processingでスクリーン座標を3D座標に変換

今、巷では技術系Advent Calendarというのが流行っているらしく、
WritingCafeでもひとつ参加してみることにしました。

まぁ、12月頭からクリスマスまでの間、各言語やテーマに沿った記事を1日1つ、
いろんな人が書いていくお祭りみたいなもの(?)です。

そんなわけでProcessing Advent Calendar 2011の12/2担当記事はこちら。

「Processingでスクリーン座標を3D座標に変換する方法」です。

Unprojectサンプルのページへ

Processingで3D空間の座標(モデル座標、オブジェクト座標)をスクリーン上の2D空間に変換するには、
screenX(x, y, z)、screenY(x, y, z)といった関数を使います。

では、スクリーン座標を3Dのモデル座標に変換するには、どうしたらいいでしょう。

そんなときにあるのがmodelX(x, y)、modelY(x, y)といった関数――だといいのですが、そうではなく。
modelX、modelYは単にローカルなモデル座標をワールド座標に変換してくれるだけです。
なので、あいかわらず3D空間の値しか取れないのです。

OpenGLの関数、gluUnprojectを使えばスクリーンの2D空間の座標を3Dの座標に変換できますが、それだけのためにOpenGL関数やOpenGLレンダラーを使うのはどうも……。

というわけで、今回はこれをProcessingの関数のみで計算してみます。

2D→3D変換の前に、まず3D→2D変換のための行列を作りましょう。

// モデル座標をスクリーン座標に変換する行列を取得
PMatrix3D getModelToScreenMatrix() {
  PMatrix3D projection = new PMatrix3D();
  matrixMode(PROJECTION); getMatrix(projection);
  
  PMatrix3D modelview = new PMatrix3D();
  matrixMode(MODELVIEW); getMatrix(modelview);
  
  PMatrix3D viewport = new PMatrix3D();
  viewport.m03 = viewport.m00 = width * 0.5f;
  viewport.m13 = viewport.m11 = height * 0.5f;
  
  PMatrix3D m = new PMatrix3D(modelview);
  m.preApply(projection);
  m.preApply(viewport);
  return m;
}

モデルビュー行列やプロジェクション行列の取得方法は、リファレンスには載っていませんが、
matrixModeとgetMatrixを組み合わせると取得可能です。[1]
ちゃんとviewport行列も作っているところがポイント。

  • [1]PGraphics3Dのメンバ変数を PGraphics3D p3d = (PGraphics3D)g; 経由で、p3d.modvwとか、p3d.projectionとか、p3d.cameraといった行列を直接参照する方法もあります。

3D→2D変換の行列ができれば、目的の8割は達成したといえます。

// モデル座標をスクリーン座標に変換
PVector projectScreen(PVector v) {
  PMatrix3D m = getModelToScreenMatrix();
  return matrixCMult(m, v);
}

projectScreen関数内では、getModelToScreenMatrixで取得した3D→2D変換行列を
matrixCMult関数を使ってモデル座標のベクトルを掛けあわせた後、wで割っています。

// 行列で変換したベクトル(wで割ったもの)を返す
PVector matrixCMult(PMatrix3D m, PVector v) {
  PVector ov = new PVector();
  ov.x = m.m00*v.x + m.m01*v.y + m.m02*v.z + m.m03;
  ov.y = m.m10*v.x + m.m11*v.y + m.m12*v.z + m.m13;
  ov.z = m.m20*v.x + m.m21*v.y + m.m22*v.z + m.m23;
  float w = m.m30*v.x + m.m31*v.y + m.m32*v.z + m.m33;
  if(w!=0) ov.mult(1.0f / w);
  return ov;
}

中身はともかく、projectScreen関数は、以下のように呼び出せばOK

PVector modelPos = new PVector(10, 20, 30);
PVector screenPos = projectScreen(modelPos);

screenPos.x、screenPos.y に それぞれ screenX や screenY 関数で計算したときと
ほぼ同じ値が入り、スクリーンに投影できていることが分かります。

これをふまえて、2D→3D変換の関数を作ります。

// スクリーン座標をモデル座標に変換
PVector unprojectScreen(PVector v) {
  PMatrix3D m = getModelToScreenMatrix();
  m.invert();
  return matrixCMult(m, v);
}

違うのは関数名の他に1行だけ。

m.invert();

と、変換行列を逆行列にしているだけですね。

unprojectScreen関数を使うときは、以下のように呼び出せばOK

PVector mouse3D = unprojectScreen(new PVector(mouseX, mouseY, 0));

さらにunprojectScreenでZ値を1にした座標やカメラ位置と
mouse3Dの差分ベクトルを使ったりすればマウス位置から3D空間に飛ぶ直線が得られます。

詳しいところはサンプルをいじって確認してみましょう。

# 本当は別の少しクレイジーなネタを書くつもりだったのですが、
# そのためにこの変換処理の説明が必要だったので、こちらを記事にしちゃいました。



Trackback(0)