Imaginary Code

from kougaku-navi.net

Processingで3Dグラフィックスを扱う上での注意点

 Processingのグラフィックスの描画系は内部でOpenGLを利用していますが、ProcessingのAPIは特有の癖があるのでOpenGLと同じ感覚でやっていると時々戸惑うことがあります。3DグラフィックスまわりのProcessing特有の仕様について、気をつけておくべき点をここにメモしておきます(Processing 2.2.1において動作確認)。

(2017/7/19)主に行列のところを加筆しました。
(2019/5/15)文章を少し手直ししました。本質的な意味は変えてません。
(2020/11/19)座標系の向きの図を更新し、プロジェクション行列について追記しました。

座標系

  • Processingは左手系。
  • 視点を中心とする座標系(カメラ座標系)の座標軸の向きは、x軸が右方向、y軸が下方向、z軸が手前方向となっている。
  • この座標軸の向きの違いがプロジェクション行列に影響する(後述)。



3Dグラフィックスで扱われる行列とその取得方法

  • Processingの内部で以下の行列が管理されている。
    • プロジェクション行列(projection)
    • モデルビュー行列(modelview)
    • モデルビュー行列の逆行列(modelviewInv)
    • ビュー行列(camera)
    • ビュー行列の逆行列(cameraInv)
    • プロジェクション行列とモデルビュー行列を乗算した行列(projmodelview)
  • ワールド座標系とローカル座標系のあいだのモデリング変換を表わすモデル行列は定義されていない。モデル行列が欲しい場合は、ビュー行列の逆行列とモデルビュー行列を乗算して求めればよい。

f:id:kougaku-navi:20201119150434p:plain

  • プロジェクション行列やモデルビュー行列は、PAppletのメンバ変数である「g」から取得できる。「g」はPGraphicsクラスのオブジェクトだが、その派生クラスであるPGraphicsOpenGL(またはPGraphics3D)でキャストしてやると、modelviewやprojectionにアクセスできる。
  • 各行列への参照を取得できるので、行列の値を書き変えてジオメトリを直接コントロールすることもできる。
  • 行列の値を直接書き変える場合は、ペアになっている逆行列(modelviewの場合はmodelviewInv)のほうも更新したほうがよいようだ。
  • projectionまたはmodelviewを書き変えた場合は、projmodelviewを更新するために ((PGraphicsOpenGL)g).updateProjmodelview() を実行する必要があるようだ。
  • 行列のコピーが欲しい場合は、.get()で取得すればよい。
行列への「参照」が欲しい場合(内部のパラメータを直接書き変えたい場合)
// プロジェクション行列
PMatrix3D projection = ((PGraphicsOpenGL)g).projection;

// モデルビュー行列
PMatrix3D modelview = ((PGraphicsOpenGL)g).modelview;

// モデルビュー行列の逆行列
PMatrix3D modelviewInv = ((PGraphicsOpenGL)g).modelviewInv;

// ビュー行列
PMatrix3D camera = ((PGraphicsOpenGL)g).camera;

// ビュー行列の逆行列
PMatrix3D cameraInv = ((PGraphicsOpenGL)g).cameraInv;

// プロジェクション行列とモデルビュー行列を乗算した行列
PMatrix3D projmodelview = ((PGraphicsOpenGL)g).projmodelview;

行列の「コピー」が欲しい場合
// プロジェクション行列
PMatrix3D projection = ((PGraphicsOpenGL)g).projection.get();

// モデルビュー行列
PMatrix3D modelview = ((PGraphicsOpenGL)g).modelview.get();

// モデルビュー行列の逆行列
PMatrix3D modelviewInv = ((PGraphicsOpenGL)g).modelviewInv.get();

// ビュー行列
PMatrix3D camera = ((PGraphicsOpenGL)g).camera.get();

// ビュー行列の逆行列
PMatrix3D cameraInv = ((PGraphicsOpenGL)g).cameraInv.get();

// プロジェクション行列とモデルビュー行列を乗算した行列
PMatrix3D projmodelview = ((PGraphicsOpenGL)g).projmodelview.get();

デフォルトの視点位置

  • デフォルトではワールド座標系≠カメラ座標系であることに注意。
  • デフォルトの視点位置は以下の通り。
    • 目の位置:( width/2, height/2, (height/2)/tan(PI/6) )
    • 注視点の位置:( width/2, height/2, 0 )
  • これは、画面の左上隅が(0, 0, 0)、右下隅が(width, height, 0)にそれぞれ対応するような視点の配置になっている。ただし、これは透視投影の視野角がデフォルトの60度であることを考慮した計算のため、perspective()などで視野角が変更されるとこの関係はくずれる。



draw()開始時に行われるモデルビュー行列の初期化

  • 毎フレーム、draw()開始時に、モデルビュー行列が自動的に初期化される。
  • どのように初期化されるのかというと、モデルビュー行列(modelview)がビュー行列(camera)と同じ値になる。
  • この挙動は、ビュー行列は常に保持されていて、draw()開始時にモデル行列だけを単位行列にリセットしている、と捉えることができる。
  • ビュー行列はdraw()開始時にリセットされないため、ビュー行列に変更を加える操作(camera()やresetMatrix())を行うと、以降のフレームにも影響が出る。draw()の冒頭で毎回camera()やresetMatrix()を実行している場合は特に気にする必要はないが、draw()の外や特定のタイミングでそれらの操作を実行する場合は留意したい。

camera()の挙動

  • camera()はワールド座標系に対する視点の位置・姿勢を設定する関数である。
  • camera()を実行すると、内部で保有している5つの行列が更新される(modelview、modelviewInv、camera、cameraInv、projmodelview)
  • camera()を実行すると、パラメータで指定された視点になるようにビュー行列(camera)が設定され、モデル行列は単位行列に初期化される。その結果としてモデルビュー行列(modelview)はビュー行列に等しくなる。
  • camera()を実行すると、それ以降のフレームにおいて、draw()開始時のモデルビュー行列はcamera()で設定されたビュー行列で初期化されるようになる。ただし、camera()を実行している箇所をpushMatrix()とpopMatrix()で囲んでいた場合はこの限りではない。
  • 上方向ベクトル(upX, upY, upZ)は(downX, downY, downZ)じゃないの?という疑念がある。OpenGL由来のものだと思われるが、Y軸が下に向いているのに(0, 1, 0)で上方向を表わすのは気持ちが悪い。フォーラムでも同様の指摘がある。
  • ビュー行列の初期状態は以下の通り。
    • ここでwidthとheightは画面サイズを表わす。
    • camera()関数にデフォルトパラメータを与えた時のビュー行列がこれになる。つまり、上図のカメラ配置にするための行列。
 1    0    0       -width/2
 0    1    0       -height/2
 0    0    1    -(height/2)/tan(PI/6)
 0    0    0          1

resetMatrix()の挙動

  • resetMatrix()を実行すると、modelview、modelviewInv、camera、cameraInvが単位行列に初期化され、projmodelviewがprojectionと同じ値になる。
  • resetMatrix()を実行すると、それ以降のフレームにおいて、draw()開始時のモデルビュー行列は単位行列になる(ビュー行列も単位行列になってしまっているから)。ただし、resetMatrix()を実行している箇所をpushMatrix()とpopMatrix()で囲んでいた場合はこの限りではない。

プロジェクション行列

  • Processingのプロジェクション行列は、OpenGLやUnityのものとは異なり、2列目の要素の符号が反転している。
  • これはカメラ座標系のY軸が下向き(OpenGLやUnityでは上向き)で、正規化デバイス座標系のy軸が上向きであることに由来する。


f:id:kougaku-navi:20201119144810p:plain

透過処理がうまくいかないときは

透明物体の描画がおかしいときや、文字の背景部分が正しく透けないときは、setup()のなかに以下の一行を入れると改善する(奥のオブジェクトからレンダリングする処理の有効化)。

  hint(ENABLE_DEPTH_SORT);

さらなる探究