OpenGLは右手座標系?

OpenGLは右手座標系、DirectXは左手座標系である。
…色々な所にそう書いてあり、それを信じていたが、この前、自分が書いたコードが、それを否定する動作をして、混乱してしまった。
次のプログラムを実行すると、Z=0の赤い三角よりZ=-1の白い三角の方が手前に表示されてしまう。

#include <GL/glut.h>

void draw_triangle()
{
	glBegin(GL_TRIANGLES);
	glVertex3f(0, 1, 0);
	glVertex3f(-0.866f, -0.5f, 0);
	glVertex3f( 0.866f, -0.5f, 0);
	glEnd();
}

void display(void)
{
	glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.2, 1.0);  /* dark blue */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	/* red triangle at Z=0 */
	glColor4f(1.0f, 0.0f, 0.0f, 1.0f);	/* red */
	draw_triangle();

	/* white triangle at Z=-1 */
	glTranslatef(0.0f, 0.0f, -1.0f);
	glScalef(0.2f, 0.2f, 1.0f);
	glColor4f(1.0f, 1.0f, 1.0f, 1.0f);	/* white */
	draw_triangle();

	glutSwapBuffers();
}

int main(int argc, char *argv[])
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
	glutCreateWindow(argv[0]);
	glutDisplayFunc(display);
	glutMainLoop();
	return 0;
}
図1
GLUTのせいか?とも思ったが、これまで自分が作ったGLUTのプログラムは間違いなく右手座標系だった。

調べた所、OpenGLの初期状態は左手座標系であることがわかった。
OpenGL Spec 2.1のAppendix B "Corollaries"の所に、

15. OpenGL does not force left- or right-handedness on any of its coordinates systems. Consider, however, the following conditions: (1) the object coordinate system is right-handed; (2) the only commands used to manipulate the model-view matrix are Scale (with positive scaling values only), Rotate, and Translate; (3) exactly one of either Frustum or Ortho is used to set the projection matrix; (4) the near value is less than the far value for DepthRange. If these conditions are all satisfied, then the eye coordinate system is right-handed and the clip, normalized device, and window coordinate systems are left-handed.
とある。ここに書かれている通り、DepthRangeがnear < farであれば、Model-View MatrixやProjection Matrixが適用された後のClip CoordinatesやNormalized Device Coordinates(NDC、X,Y,Z座標が全て-1〜+1の範囲)は左手座標系であるが、Model-View MatrixやProjection Matrixをそのように設定すれば、Object CoordinatesやEye Coordinatesを右手座標系にすることが可能なのである。
DepthRangeの初期値は、near=0、far=1であり、NDCがViewport TransformationでWindow Coordinatesになる時に、Z=0が手前、Z=1が奥と扱われるので、NDCは左手座標系である。
Projection Matrixを設定する時によく用いられる、glFrustum()やgluPerspective()は、よく見るとZ座標に掛かる係数が負の値であり、これによって、Z座標の前後関係が反転され、Eye CoordinatesではZ軸の+方向が手前、つまり右手座標系になるのである。glOrtho()を用いる場合でも、引数がleft, right, bottom, top, near, farの順なので、何も考えずにglOrtho(-1, 1, -1, 1, -1, 1);とすると、Z座標に掛かる係数が負の値になり、Eye Coordinatesは右手座標系になる。
しかし、Projection Matrixの初期値は単位行列(glOrtho(-1, 1, -1, 1, 1, -1);するのと同じ)であり、Z座標の前後関係を反転させないので、OpenGLの初期状態では、Eye Coordinatesは左手座標系である。

さて、上記の引用文にある通り、OpenGLは左手座標系(left-handed)でも右手座標系(right-handed)でも良い、とのことであるが、それでは困ることがある。特に、右手座標系だったり左手座標系だったりすると、3D物体の表面が裏返しになってしまうのが困るのである。
ポリゴンの裏表は、Window Coordinatesでポリゴンの頂点が時計回りか反時計回りかだけで決まるので、例えば、右手系であることを前提にして手前(Z軸の+方向)に表、奥(Z軸の−方向)に裏のポリゴンを配置した物体は、左手系だと手前(Z軸の−方向)に裏のポリゴン、奥(Z軸の+方向)に表向きのポリゴンがある状態になる。つまり、物体の表面が物体の内側を向いてしまう。
ポリゴンが右手系で配置されているとわかっているなら、左手系で表示するならglFrontFace()を用いて時計回りが表か反時計回りが表かを逆転させれば良いのであるが、ポリゴンが右手系で配置されているか左手系で配置されているかを考慮する必要があることが面倒である。

例えば、GLUTのオブジェクトも右手座標系を前提にしている(Teapotのポリゴンの頂点が時計回りである不具合を除く。glutSolidTeapotのman page参照)ので、CULL_FACEを有効にして左手座標系で描画すると、おかしなことが起こる。

/* showing that GLUT objects are right-handed */
void display2(void)
{
	const GLfloat lightpos_for_LH[4] = {0.0f, 0.0f, -1.0f, 0.0f};	/* directional light */
	const GLfloat default_lightpos[4] = {0.0f, 0.0f, 1.0f, 0.0f};
	
	glEnable(GL_CULL_FACE);
	glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.2, 1.0);  /* dark blue */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glEnable(GL_COLOR_MATERIAL);

	/* LH drawing */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1, 1, -1, 1, 1, -1);	/* left-handed */
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, lightpos_for_LH);

	glTranslatef(-0.5f, 0.2f, 0.0f);
	glColor4f(1, 0, 0, 1);

	glRotatef(-90-20, 1, 0, 0);
	glutSolidCone(0.3, 0.7, 20, 10);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, default_lightpos);	/* to see the strangeness more clearly */
	glTranslatef(-0.5f, -0.5f, 0.0f);
	glColor4f(0, 1, 0, 1);

	glRotatef(20, 1, 0, 0);
	glFrontFace(GL_CW);	/* for glutSolidTeapot bug (see man page) */
	glutSolidTeapot(0.3);
	glFrontFace(GL_CCW);

	/* RH drawing */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1, 1, -1, 1, -1, 1);	/* right-handed */
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, default_lightpos);
	glTranslatef(0.5f, 0.2f, 0.0f);
	glColor4f(1, 0, 0, 1);

	glRotatef(-90+20, 1, 0, 0);
	glutSolidCone(0.3, 0.7, 20, 10);

	glLoadIdentity();
	glTranslatef(0.5f, -0.5f, 0.0f);
	glColor4f(0, 1, 0, 1);

	glRotatef(20, 1, 0, 0);
	glFrontFace(GL_CW);
	glutSolidTeapot(0.3);
	glFrontFace(GL_CCW);

	glFlush();
	glutSwapBuffers();
}
図2
左半分が左手座標系で、右半分が右手座標系で同じものを描画したものである。左上の円錐は、手前の表向きのポリゴンが消えて、奥の裏向きのポリゴンが見えている。左下のティーポットは、手前から光を当てるとシルエットだけになるので、後ろから光を当てており、丁度ポリゴンの法線と光源の方向が一致して明るくなっているが、見えているのはやはり奥のポリゴンであり、所々が変である。

また、ライティングのパラメーターも、右手座標系か左手座標系かに関係する。
OpenGL Spec 2.1の記述を引用すると、lightingに関して、

All computations are carried out in eye coordinates.
とあり、例えば
The current model-view matrix is applied to the position parameter indicated with Light for a particular light source when that position is specified.
なので、Projection Matrixで右手座標系にするなら、光源の位置を含め、ライティングに関するパラメーターは右手座標系の前提で決めないといけない。
例えば、手前から光を照射する場合、右手座標系だとZ軸が+の方に光源を置くことになるが、左手座標系だとZ軸が-の方に光源を置くことになる。

ここで注目すべきは、Lighting parametersのPOSITIONの初期値は(0.0, 0.0, 1.0, 0.0)、SPOT_DIRECTIONの初期値は(0.0, 0.0, -1.0)であることだ。つまり、OpenGLの初期状態では、Z軸が+の方向から−の方向へ平行光が照射しており、スポットライトの反射方向はZ軸が−の方向であり、右手座標系を前提とした設定になっているのである。

そういう意味では、やはりOpenGLは右手座標系で使うのが基本と言えるのではないだろうか。

右手座標系の方が数学で見慣れて利便性が高いし、glFrustum()やgluPerspective()を使って右手座標系にして使うことの方が多いので、3D物体も右手座標系で作るべきだと思った。

参考URL
http://stackoverflow.com/questions/4124041/is-opengl-coordinate-system-left-handed-or-right-handed


さらに、Mac OS Xでは、左手座標系にすると、点光源の場合に正しく表示されないことに気付いた。

/* point light from negative Z causes trouble on Mac OS X */
void display3(void)
{
	const GLfloat lightpos_for_LH[4] = {0.0f, 0.0f, -1.0f, 1.0f};	/* point light */
	const GLfloat lightpos_for_RH[4] = {0.0f, 0.0f, 1.0f, 1.0f};

	glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.2, 1.0);  /* dark blue */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glEnable(GL_COLOR_MATERIAL);

	/* LH drawing */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1, 1, -1, 1, 1, -1);	/* left-handed */
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, lightpos_for_LH);

	glTranslatef(-0.5f, 0.5f, 0.0f);
	glColor4f(1, 0, 0, 1);
	glScalef(0.5f, 0.5f, 1.0f);

	glNormal3f(0.0f, 0.0f, -1.0f);
	draw_triangle();

	glLoadIdentity();
	glTranslatef(-0.5f, -0.5f, 0.0f);
	glColor4f(0, 1, 0, 1);

	glRotatef(-20, 1, 0, 0);
	glutSolidTeapot(0.3);

	/* RH drawing */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1, 1, -1, 1, -1, 1);	/* right-handed */
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, lightpos_for_RH);

	glTranslatef(0.5f, 0.5f, 0.0f);
	glColor4f(1, 0, 0, 1);
	glScalef(0.5f, 0.5f, 1.0f);

	glNormal3f(0.0f, 0.0f, 1.0f);
	draw_triangle();

	glLoadIdentity();
	glTranslatef(0.5f, -0.5f, 0.0f);
	glColor4f(0, 1, 0, 1);

	glRotatef(20, 1, 0, 0);
	glutSolidTeapot(0.3);

	glFlush();
	glutSwapBuffers();
}
このコードをMesa 7.4.4で表示すると、次の左の画像(図3)のようになるが、Mac OS X 10.7.5のOpenGLで実行すると、右の画像(図4)のようになった。
図3 図4
光源の位置が-Z方向で、ポリゴンの法線も-Z方向だと、ライティングの処理が省略される仕様なのかと思ったが、光源の位置が-Z方向で無限遠(light positionの4つ目の値が0)だと省略されない(図2)し、Lighting Model ParametersのLIGHT_MODEL_AMBIENTで決まるglobal ambientも反映されず、真っ黒になってしまっているので、筆者はMac OS XのOpenGLの不具合である可能性が高いと推測している。
これは、左手座標系でライティングを行うのに致命的である。

こういう不具合が修正されずに放置されているということは、Eye coordinatesを左手座標系で使用する人がほぼ皆無であることを示しているのではないだろうか。
OpenGLのデフォルト設定は左手座標系であるが、OpenGLを左手座標系で使うことは無謀と言っても過言ではないと思った。