Results tagged “OpenGL”

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

iPhoneでもOpenGLを動かしてみたいと思って、知人から入手したiPhone3Gであるが、3Gというのが曲者だった。

iPhone3Gは、OpenGLアプリを作成するのにいくつかの制約がある。
・OpenGLES2.0が使えない
 OpenGLES1.1しか使えない。
・GLKitが使えない
 GLKitはiOS5以降に含まれているが、iPhone3GはiOS4.2.1までしかバージョンアップできない為。
・ステンシルバッファが使えない

それに対し、iPhone3Gの1つ次の3GSは、OpenGLES2.0が動く。これは、3GS以降のGPUは全てPowerVRのSGXシリーズであるのに対し、3GはPowerVRの"MBX Lite 3D"であることが関係しているようだ。
また、3GSにはiOS5がインストールできるので、そうすればGLKitが使える。
そして、3GSはステンシルバッファが使える。

iPhone3GはARMv6アーキテクチャーである為、Xcode 4.4.1を使用しているが、これに含まれる、iOS Applicationの"OpenGL Game"というテンプレートは、付属のiPhoneシミュレーターでは動いたので、簡単にiPhone3GでOpenGLを始められそうと思ったが、このテンプレートはGLKitを使用しているため、iPhone3Gでは動かなかった。
しかも、このテンプレートはOpenGLES2.0にも対応しているため、冗長である。

その為、GLKitに依存していないOpenGLES1.1用のテンプレートをインターネット上で探した。その結果、以下を発見した。
(1) Xcode 3.xのOpenGL ES Application テンプレート
 OpenGLES2.0も使うようになっているので、シミュレーターでは表示されるがiPhone3Gの実機では表示されないオブジェクト(物体)がある。
 但し、depth bufferは用意されていないらしい。
 また、Xcode 4では開けない。
(2) OpenGL ES 道場(1) - こじ研(携帯メディア) のGLBaseプロジェクト
 Xcode 3.xのテンプレートをベースに、depth bufferが追加されており、多少OpenGLES1.1向けに整理されている。但し、多少GLES2.0用の残骸が残っている。
(3) jlamarche/iOS-OpenGLES-Stuff · GitHub紹介記事)のSimple OpenGL ES 1.1 example
 OpenGLES1.1のサンプルプロジェクト。下記(4)のテンプレートを使用したプロジェクトと類似している。但し、Xcode 4.4.1で確認する限り、そのままではシミュレーターでも動かない。
(4) jlamarche/iOS-OpenGLES-Stuff · GitHub紹介記事)のOpenGL ES 1.1 Project Template
 OpenGLES1.1に特化したテンプレート。Quaternionを扱うためのマクロや、gluLookAt()や、アセンブラで書かれた行列積のマクロが用意されており、専門的な香りがする。
 但し、Xcode 3用のテンプレートであり、Xcode 4以降では開けない。

これらの内、(1)は使用するメリットがほとんど無いので、(2)-(4)について、実際にiPhone3Gで動作するまでに行ったことを記録する。

■(2)-(4)共通の、iPhone3Gのための手順
・"TARGETS"の"Build Settings"の"Architectures"を、"Standard (armv7)"から"armv6"に書き換える
・(推奨)同じく"Build Settings"の"LLVM GCC 4.2 - Code Generation"の所の"Optimization Level"を、"None [-O0]"以外にする
("None [-O0]"にしていると、整数型の割り算や剰余が、libgccの___divsi3や___modsi3を使うコードになることがあり、実機で"Symbol not found: ___divsi3"等のエラーになってハングアップする)

■(2) GLBaseプロジェクトの使用手順
特に何もいじらなくても、そのまま動く。
GLBaseViewController.mのdrawFrameを書き換えれば、OpenGLのコードで好きな描画をさせることができる。

なお、ディレクトリーツリーのルートの"GLBase"をrenameすれば、ディレクトリー内の"GLBase"を含むファイル名を一斉にrenameできる。

■(3) Simple OpenGL ES 1.1 exampleの使用手順
・"git clone https://github.com/jlamarche/iOS-OpenGLES-Stuff.git"等として取得
・(推奨)Build Settingsの画面で"Validate Settings"を実行し、修正を許可
・TARGETSのBuild SettingsのBuild OptionsのCompiler for C/C++/Objective-Cを"LLVM GCC 4.2"に変更(Default Compilerだと実機用のコンパイルがエラーになる)
・そのままではPart6ProjectAppDelegate.applicationDidFinishLaunchingが呼ばれないため、以下のパッチを適用、または同様に変更

--- Original/iOS-OpenGLES-Stuff/Simple OpenGL ES 1.1 example/Classes/Part6ProjectAppDelegate.m
+++ tmp/Simple OpenGL ES 1.1 example/Classes/Part6ProjectAppDelegate.m
@@ -23,6 +23,9 @@
GLViewController *theController = [[GLViewController alloc] init];
self.controller = theController;
[theController release];
+
+ self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
+ self.window.rootViewController = self.controller;

GLView *glView = [[GLView alloc] initWithFrame:rect];
[window addSubview:glView];
--- Original/iOS-OpenGLES-Stuff/Simple OpenGL ES 1.1 example/main.m
+++ tmp/Simple OpenGL ES 1.1 example/main.m
@@ -8,11 +8,12 @@


#import
+#import "Part6ProjectAppDelegate.h"

int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
- UIApplicationMain(argc, argv, nil, nil);
+ UIApplicationMain(argc, argv, nil, NSStringFromClass([Part6ProjectAppDelegate class]));
[pool release];
return 0;
}

これで、カラフルな12面体が回転する。
GLViewController.mののdrawFrameを書き換えれば、OpenGLのコードで好きな描画をさせることができる。

■(4) OpenGL ES 1.1 Project Templateの使用手順
これは残念ながらXcode3用のテンプレートであり、Xcode4では使えない。Xcode3とXcode4のテンプレートは互換性が無い上、Xcode4用に書き換えるのも困難なのである。
参考URL:XCode4のプロジェクトテンプレートが作れない! - とっくりばー
Xcode3でこのテンプレートを使ってプロジェクトを作成するのが1つの方法だが、ファイル名や一部文字列を置換するだけなので、そういうスクリプトを作っても良いし、上記URLに載せられているRubyスクリプトでもプロジェクトファイルのコピー&置換ができる。

1. 上記URLのReplacer.rbを使う等により、テンプレートからプロジェクトを作成
2. なぜかGLView.mの場所が間違っているので、Classes/に移動する
3. (推奨)Build Settingsの画面で"Validate Settings"を実行し、修正を許可
4. (推奨)TARGETSのBuild SettingsのBuild OptionsのCompiler for C/C++/Objective-Cを"LLVM GCC 4.2"に変更(Default CompilerだとOpenGLCommon.hのアセンブラを用いたマクロがコンパイルエラーになる)

これで、GLViewController.mののdrawFrameに何かを書けば、OpenGLのコードで好きな描画をさせることができる。
何も書かないと画面が真っ暗になるので、試しに何か表示するなら、drawFrameをiOS-OpenGLES-Stuff/Simple OpenGL ES 1.1 example/Classes/GLViewController.mのものに置き換えてみても良いし、画面が暗い青になるだけで良ければ、1行目の"glColor4f"を"glClearColor"に書き換えても良い。

AndroidでOpenGLしてみた(2)

またしても運良くAndroidタブレットを借りることができたので、この間のOpenGLのテストアプリをAndroidに移植してみた。

・実行画面(シミュレーター)

・Android用実行ファイル
AndroidTestP1.apk

・ソースコード
AndroidTestP1src.tar.gz

筆者はAndroidに疎く、見よう見まねで何とか動かしたというだけのものである。
以下、今回苦労した点を記録する。

・ADT付属のemulatorではOpenGLの動作が完全には再現されない
最も時間を取られたのは、ポリゴンを重ねて描画した時のシミュレーターの動作だった。前回のエントリーに書いた影行列を設定してオブジェクトを描画すると、上の画像でもそうなのだが、影が所々にしか塗られなかったり、汚いまだら模様になったりする。
色々試した結果、DEPTH_TESTが有効で、2つのポリゴンの面が重なるように近くにあると正しく動かないようであることがわかった。基本的にはdepth値の精度に起因する誤差による不具合だと思うが、表面と裏面を重ねてCULL_FACEを有効にしても表面が裏面に隠されて何も描画されない部分が出てくるし、色もおかしくなるなることがある((R,G,B)=(1,0.5,0)のオレンジのポリゴン表裏2枚をゆっくり重ねると、だんだん黄色になることを確認した)。
ふと、実機ではどうなるのだろう、と思って実機で動かしてみると、何の問題もなく影が表示されて、力が抜けた。
Android SDKで動作がおかしかったら、まずは実機の動作を確認することだと思い知った。

・glMaterialfv()の引数はGL_FRONT_AND_BACKでないと反映されない
GL10#glMaterialfv()の引数のfaceにGL10.GL_FRONTを指定すると、ambient colorやdiffuse colorの設定が反映されなかった(シミュレーター、実機共)。まさかそれが誤りだとは思わなかったので、そのことが原因だと特定するのに時間がかかった。何なんだろうこれは。
って、OpenGL ESの仕様なんだな。glGetError()すればGL_INVALID_ENUMが返されるのですぐにわかるのだが、glGetError()の値をこまめに見る習慣が無かったのが致命的な敗因だった。

・GLSurfaceViewのデフォルト設定ではステンシルバッファが存在しない
そのこと自体は簡単に試してみればすぐわかることであり、android.opengl.GLSurfaceViewのreferenceにも

By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, with at least a 16-bit depth buffer and no stencil.
とあるので、setEGLConfigChooser()すればいいこともすぐにわかる。
そこで、
setEGLConfigChooser(8, 8, 8, 8, 16, 8);
とすると、実機ではステンシルテストが動くようになるが、シミュレーターではステンシルバッファがサポートされていないので、
Unfortunately, AndroidTestP1 has stopped.
と出てアプリがクラッシュする。シミュレーターでは、ステンシルバッファ付きのEGLConfigが得られないことが原因である。その場合はステンシルバッファ無しのEGLConfigで継続実行するようにしようと思ったが、setEGLConfigChooserの呼び出しをtry/catchで括ってみても、ここですぐにeglChooseConfig()によるEGLConfigの検索が行われる訳ではないので、ここでは例外が発生しない。setEGLConfigChooser()はsetRenderer()より前に行わないといけないとなっているので、実際に例外が発生してから引数を変えてsetEGLConfigChooser()をやり直す訳にもいかない。
実機で動作すればそれで良い、という人も居るかも知れないが、シミュレーターで全く動作確認できなくなるのは不便である。何より、筆者は普段Androidの実機を所有しておらず、基本的にシミュレーターしか使わないのである。
すると、EGL10に依存するGLSurfaceView.EGLConfigChooserインターフェースを、ステンシルバッファを有効にするためだけに実装しないといけないことになる。
…という結論に達するまでに、結構時間が掛かった。
GLUTなら前回のエントリーに書いたようにglutInitDisplayMode()の引数にGLUT_STENCILを加えれば済む所、GLSurfaceViewだとEGLを呼び出すコードを自分で用意しないといけないとは、面倒である。自由度が高いことに、実用的なメリットはあると思うが、すっきりしない。

・GL10クラスにglGetMaterialfvメソッドが無かった
COLOR_MATERIALを有効にするとambientやdiffuseが変化してしまうので、後で戻す為に現在のambientやdiffuseの値を取得しようとしたら、glGetMaterialfvメソッドが無かった。GL11クラスにはあったが、GL10のインスタンスからGL11のインスタンスを取得する方法が見つからず、ネットで検索するまで、単に(GL11)でキャストすれば良いとはわからなかった。

Javaでは、AWTのGraphicsクラスのインスタンスを(Graphics2D)でキャストしてGraphics2Dのインスタンスを得るようなことがよくあるが、クラスの継承関係からするとGraphics2DがGraphicsのサブクラス、GL10とGL11についてはGL11 implements GL10であり、いずれもサブクラスへのダウンキャストなのだが、そうするのが正しいことは何を見ればわかるのだろうか?(一般にスーパークラスへのキャストは可能だがサブクラスへのキャストは可能とは限らない)

・例外発生時の解析手段がわからない
試行錯誤中に誤ってnull参照するコードを埋め込んでしまった時、デバッガで例外をキャッチして停止させても、どこでnullアクセスしたのかさっぱりわからず、原因コードを特定するのにすごく時間を取られた。EclipseのDebugモードでは、
(Suspended (exception NullPointerException))

(Suspended (exception RuntimeException))
と出るだけである。画面上で"Unfortunately, AndroidTestP1 has stopped."と出るだけより1つは情報が加わるが、それでピンと来ることは困難であり、結局、Log.dやSystem.out.printlnやtry/catchを使いまくって原因箇所を見つけるのでは、何の為にデバッガを使ってるのかわからない。
まさかAndroid SDKはそういうものなのだとは思わないが、printfデバッグでもすぐ見つかるだろうと思っていたので、Android SDKでのまともな解析手段を探す気力は起きなかった。しかし、結局数時間調べる羽目になって、後悔すると共に、二度とAndroidでOpenGLしたくないと、二度くらいは思ったと思う。

・視点を動かしたので、トラックボール風の回転動作を変更する必要があった
今回、ついカメラ位置を自動的にオブジェクト中心に回転するようにしてしまったが、そのせいで、前回Android用に作成した、タッチドラッグ(スライド?)による回転動作がそのままでは使えなくなり、それを視点の回転に対応させる方法にかなり悩んでしまった。

画面上のドラッグ操作をトラックボールの回転操作のように扱うのは、下の図の手前の矩形のように、画面が球面上の(x,y,z)=(0,0,1)の位置(手前)に、画面の上方向がY軸が上になるように接しており、画面上のドラッグはその球面上をなぞっているものとして、ドラッグ開始位置と終了位置からトラックボールの回転軸と回転角を計算することによって実現していた。
SVG image
これは視点がZ軸上にある前提によって成り立っているものであり、視点が変わると、上の図の左上の矩形のように、画面がそちらに張り付いているものとして計算しないといけない。

最初は、一方の画面の法線をもう一方の画面の法線に移す回転軸と回転角度(クォータニオン)を求めて、ドラッグの開始終了位置から求めた回転軸と回転角度にそれを掛け合わせるか何かして回転を合成すれば良いのではないかと思ったが、どうしても2つの画面の縦軸同士、横軸同士を対応付ける方法がわからなかった。
ドラッグの回転軸を移すだけでは、画面右方向へのドラッグが画面右方向へのドラッグとして保たれるとは限らないので、少なくとも、画面間の法線を移す回転軸と回転角度の他に、法線を移した後に、画面の上が画面の上に対応するように画面上で回転させる角度が必要なのである。
この文章を書いている途中に、またちょっと考えればできそうな気がしてきたが、ドラッグ回転軸を求めてからの変換はクォータニオンベースでできないと意味が無いこともあるので、またドツボにはまる気がする。もしもう一度頭の中がクォータニオンまみれになることあったりしたらついでに考えてみることにする。

今回は結局、ドラッグの開始位置と終了位置をもう一方の画面に移動してから、ドラッグの回転軸を求めるようにした。上を+Y方向とする画面上の点(x,y,1)を、上をup=(upx,upyupz)、手前をeye=(eyex,eyey,eyez)とするもう一方の画面上の(x',y',z')に移動させる変換行列Mは、(1,0,0)がupとeyeの外積、(0,1,0)がup、(0,0,1)がeyeに変換されることから、
[up×eye up eye]
だとわかる。行列の積やsin()/cos()も無いので計算量が少ないし、シンプルでわかり易いので、実用的にはこの方法が最適なのかも知れない。

・意外と遅くなった
前回パソコンで描画したのと同じポリゴン数では、極端に遅くなった。PCでは15fpsで動かしてもCPU負荷は3%程度なのだが、Androidタブレットでは3fpsくらいしか出なかった。今回使用したAndroidタブレットのCPUやGPUは不明だが、ベンチマークテストではNexus OneよりCPU性能も3D性能も遥かに上回る機種である。Androidタブレットは、画面の見た目がパソコンのようでも、やっぱりパソコンとは処理速度が2桁くらい違うのだろうか。

影を付けるのをやめると速くなるので、ライティングによって影に着色してるのが重いのかと思ったが、ライティングを無効にして影描画してもあまり変わらなかった。
カリングを有効にしたり、depth testを有効にしたり無効にしたりしても体感的には変わらなかった。つまり、GPUによるポリゴンの着色処理に時間が掛かっているのではなく、depth testや表裏判定に至る前の処理に時間が掛かっているのだと思う。

ポリゴン数を増減すると見た目にはっきりと速くなったり遅くなったりするので、ポリゴン数に依存する処理に時間が掛かっていることはわかった。従って、例えばシーングラフを作っている為に、1回の描画で何度もDrawElementsしていることは問題ではなさそうである。

OpenGL ES 1.1ではvertex arrayが使えないので、glVertexPointerやglNormalPointerを使って頻繁にvertex bufferを切り替えているが、このやり方がまずくて不必要に遅くなっている可能性が高い。現在のコードはGL10クラスを使ったサンプルコードを流用しているが、GL10クラスにはBindBufferもBufferDataも無い(GL11やGLES11にはある)ので、vertex bufferのデータがGPUに転送されておらず、描画する度に全頂点のデータがGPUに転送されている気がするのである。

OpenGLでオブジェクトに影を付けようとして、最初に思い付いた方法は、地面に投影する変換行列を作って、同じオブジェクトを黒で描画することだったが、それが意外と難しかった。

そこで調べてみたが、オブジェクトへの影の付け方には色々な方法があるようで、影オブジェクトを別に作成する方法を除くと、大体次の3通りが多いようだ。
・変換行列による平面への投影
・シャドウボリューム法
・シャドウマッピング法
正確に説明できる自信が無いので、説明は省く。とりあえずわかったのは、簡単に実装できる方法は無いということだった。
平面上にできる影であれば、やはり行列による投影変換で描画するのが最も簡単なので、引き続き、納得できるまで取り組むことにした。

・結果
object drawn with shadowobject drawn with shadowobject drawn with shadow
ソースコード

光源の位置をL:(Lx,Ly,Lz,1)とすると、任意の3次元座標を、法線(a,b,c)が光源側を向く平面ax+by+cz+d=0に投影する行列は、次のような行列であることは、色々な所に書かれている。
shadow matrix
一応、これを筆者なりに求めてみる。

L=(Lx Ly Lz Lw)T : 光源
P=(x y z 1)T : 物体の位置
P'=(x' y' z' 1)T : 平面 ax+by+cz+d=0 に投影されたPの位置
N=(a b c d)T : 平面のパラメーター、但しNT L > 0(法線(a,b,c)が光源側を向くことより)

とし、P' = M P を満たす行列 M を求めることを考える。
P'はLとPを含む直線上にあるので、スカラー媒介変数 t を用いて
P' = L + t(P-L) --- (1)
と表せる。P'は平面上にあるので、
NTP' = 0
成り立つ。これより、
NT P' = NT L + t NT (P-L) = 0
なので、t = - NTL / (NT(P-L)) とわかる。NTL > 0なので、
t = NT L / (NT(L-P))
としておく。このtを(1)に代入し、両辺をNT(L-P)倍すると、
NT(L-P) P' = NT(L-P)L + NTLP - NTLL
= NTL P - NTP L
= NTL P - L NTP ※(NTPはスカラーなのでこのように入替可能)
= (NTL - LNT) P
となる。ここで、P'の4つ目の要素をscale factorだとすると、P'とそれをスカラーk倍したk P'は同じ位置を表すので、P'とNT(L-P) P'も同じ位置を表している。従って、Mは NT(L-P) P' = M P を満たすMでも良いので、Iを単位行列として
M = (NTL I - LNT)、つまり
shadow matrix
と求まる。


この行列をmodel view matrixに掛け合わせて、描画した地面に重ねて影を描画すると、影にノイズが出たり、全く影が表示されなかったりすることがある。
object drawn with shadow
少しでも影を浮かせると正常に表示されるが、全く同じ位置に重ね書きしようとすると、depth値の丸め誤差の都合で、所々に地面より向こうだと判断されてしまうフラグメントが発生するのが原因である。(Zテストで落とされるので、ブレンディング描画にしても解決しない。)
地面と全てのオブジェクトの影を先に描くことが可能なら、depth testを無効化して影を描画することも考えられるが、こういう時はpolygon offsetを設定するのがOpenGLの定跡であるので、今回もそのようにした。

glPolygonOffset(-1, -1);
glEnable(GL_POLYGON_OFFSET_FILL);
PolygonOffset()の引数の意味は難解なのだが、大抵の場合、少し向こう側ということにするなら(+1,+1)、少し手前ということにするなら(-1,-1)とすれば良いとされている。
このソースコードでは、手前に見える(地面に隠されない)はずの影を描画するための設定なので、(-1,-1)としているが、逆に地面に対してPolygonOffset(+1,+1)としても良い。


影を真っ黒で描画してしまうと、環境光で多少は照らされるはずの地面が全く見えなくなって不自然なので、影は半透明の黒で、つまり多少は地面が透けて見えるように描画したい。
その為には、αブレンディング描画をすれば良い。

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
これだけでオーソドックスなαブレンディングが有効になる。


影をαブレンディング描画をすると、平面に投影したポリゴンが重なると、影が2重になってしまう。
object drawn with shadow
このオブジェクトはいくつかのパーツを重ねて描画しているので、パーツ毎の影が重なるとこのようになる。
物体が半透明でない限りは、これは不自然である。これを防ぐには、ステンシルバッファを使うのが最善だと思う。
OpenGLのステンシルはマスクのような役割をし、ステンシルテストをパスしたフラグメント(viewport上のピクセル)を描画しながら、描画した位置のステンシル値を更新することができる。
例えば、このようにする。

glEnable(GL_STENCIL_TEST);
glClear(GL_STENCIL_BUFFER_BIT);
glStencilFunc(GL_NOTEQUAL, 1, 1);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
描画をステンシル値が0のピクセルに限りながら、描画したピクセルのステンシル値を1に置き換えることにより、どのピクセルも2回描かれないようにするものである。


無限に広がる平面でなく、有限の地面を描画する場合、影が地面をはみ出すと不自然である。影が地面をはみ出さないようにするには、地面が四角形であればOpenGLのclip planeを無理矢理使う方法も考えられるが、今回はこれもステンシルテストで解決した。

glEnable(GL_STENCIL_TEST);
glClear(GL_STENCIL_BUFFER_BIT);
glStencilFunc(GL_ALWAYS, 1, 1);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
/* 地面の描画 */

glStencilFunc(GL_EQUAL, 1, ~0);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
/* 影の描画 */
glDisable(GL_STENCIL_TEST);

地面を描く時にステンシル値を1に置換し、影の描画をステンシル値が1のピクセルに限りながら、影を描画したピクセルのステンシル値を+1することにより、地面がある場所のみ描画するのと影が2重に描かれないのを実現している。


なお、影を描画する際にはdepth maskによってdepth bufferへの書き込みを停止するのが、OpenGLにおける影描画の定跡である。

glDepthMask(GL_FALSE);
これによって、depth buffer上で影の位置が地面より少し盛り上がるのを避けることができる。
そのためにも、上記のpolygon offsetの件は、地面をPolygonOffset(+1,+1)して描画するより、影をPolygonOffset(-1,-1)して描画する方が適切だと思う。

GLUTを使うことによって移植性を確保しながらOpenGLのプログラム作りの練習を進める中、VMWare+FreeBSD+Mesa 7.4.4の環境でもなんとかOpenGLのシェーダーが動くことがわかったので、続いて、シェーダーを使って鏡面反射っぽいことをしてみる。

・ソースコード
polygon_sphere_test4.c

・コンパイル方法
前のエントリー参照

・実行画面

ちょっとわかりづらいが、画面中央にある球に、周囲の景色が映り込んでいるつもりである。今回の環境マッピングはcube mappingで行っているので、景色の背景画像は巨大な立方体の面の内側に貼られている感じになっている。

背景画像を変えたりもしてみたが、やっぱりわかりづらい。

床が格子模様、天井が同心円模様、側壁が波模様のつもりである。
きっと、意味不明な絵だからであろう。

絵心が無い癖に、プログラムで背景を生成しようと思ったのが間違いだった。

・操作方法
画面上をドラッグすると、背景が回転し、球への映り込みも同時に変化する。
'c'を押すと、背景画像が切り替わる。


GLSLによるshaderのコードの作成には、「Win32APIによるOpenGL 3Dプログラミング 」(工学社)という本を多分に参考にした。
Vertex shaderのコード(initShader()内のvertexShaderSource[])にmatrixが増えたので、後で読む為にそれぞれの意味をメモしておく。

modelViewMatrix
オブジェクト座標系の各頂点の法線ベクトルを回転させて視点座標系にするためのModel-View変換行列
参考:OpenGL 2.1 Spec.のFigure2.6
viewTransposeMatrix
視点が原点から離れている分、平行移動させる行列
視点から物体の頂点までのベクトルを得るのにこれを使用し、そのベクトルを法線ベクトルで反射させて、環境マッピングのベクトルを計算している
modelViewProjectionMatrix
Z軸方向の必要な範囲を[-1,1]に収めるための、いわゆるProjection Matrix
glFrustrum()で得られる行列をそのままshaderに引き渡すのに使用している
environmentRotateMatrix
環境を回転させる行列
環境マッピングの参照ベクトルを回転させるのに使用している

Shaderの記述に用いるプログラミング言語であるGLSL(OpenGL Shading Language)のバージョンは、1.5以降と1.2とではかなり異なり、1.2は時代遅れのようである(参考書籍では4.0)が、筆者の手持ちのMacOSX 10.7+GLUTの環境では1.2しか使えないようであるため、今回はGLSL 1.2で記述している。それでも、今回のようにtextureCube()でcube mappingはできるし、反射ベクトルを求めるreflect()や屈折ベクトルを求めるrefract()も使えるので、十分に使い勝手があると思った。

前回のVAOに続き、今回も、glUseProgram()等のOpenGL 2.x標準のshader APIをOpenGL 1.3相当のMesaで動かす為に、glutGetProcAddress()による拡張機能のリンクを使いまくって、何とかOpenGL 2.1用のコードをそのまま動かすことに成功した。
しかし、それぞれの拡張APIとOpenGL 2.x以降の標準APIとの違いや、拡張APIの中でも、名前の後ろに"ARB"が付いているAPIと"EXT"が付いているAPIとの違いとかは全く調べていない。ただ、試行錯誤したら動いたというだけである。
1つ分かったのは、glUseProgramをglUseProgramObjectARBで代用しているが、

glUseProgram(0);
(shader program不使用への切り替え)のつもりで
glUseProgramObjectARB(0);
とすると異常動作するようである。幸い、今回はglUseProgram(0)しなくて済んだので、MesaではglUseProgram(0)を無処理に置き換えて回避した。

一応、Mesaのソフトウェアレンダリング環境でも動作したが、グラフィックハードウェアでユーザープログラムを処理する為の仕組みであるshaderをソフトウェアで処理するだけあって、極端に遅い上に、表示が汚い。上の画像はMac OS X上の実行結果であるが、同じMacでVirtualBox+FreeBSD上で実行すると、次のようになる。

ソフトウェアレンダリング環境でも動くようにシェーダーを作るというのはあまり意味が無いと思った。

続いて、vertex arrayにテキスチャーの頂点座標も含めてみる。

・ソースコード
polygon_sphere_test3.c

・コンパイル方法
前のエントリー参照

・実行画面

例によって、ドラッグで回転する。

前回は頂点座標の配列と法線ベクトルの配列を別にして、それぞれのVBOを作るような形にした(法線ベクトルは不要になったのでコメントアウトした)が、今回は頂点座標と法線ベクトルとテキスチャー座標を1つの配列にまとめて、VBOも1つにした。つまり、

struct {
 GLfloat vertex[3];
 GLfloat normal[3];
 GLfloat texCoord[2];
};
こういう型の配列である。こうすると、それぞれを別の配列にするのと比べ、少なくとも1つの頂点の処理中は頂点座標も法線ベクトルもテキスチャー座標も同じキャッシュラインに乗る確率が上がるので、CPUやGPUのメモリキャッシュの効率が上がると言われている。

OpenGLのVAOを使ってみた

前回のコードは、一連のコマンドのコンパイルもせず、vertex arrayやbuffer objectも使わずに、描画の度に毎回大量の命令を行っており、大変非効率なので、vertex arrayを使って少しマシにする。

・ソースコード
polygon_sphere_test2.c

・コンパイル方法
前のエントリー参照

・実行画面

※今回は処理負荷軽減が主旨であり、ポリゴンの各頂点を使い回すため、各頂点の法線ベクトルは頂点座標と同じ値にしているので、表面が前回よりツルツルになっている。

Vertex array初体験の筆者にとっては、vertex arrayとbuffer objectとvertex array object(VAO)の関係がややこしい。今週理解した限りでは、おそらく、次のような感じだと思う。


Vertex array
頂点座標データを含む配列。"Vertex"と言いながら、頂点の法線ベクトルや頂点の色情報やテクスチャーの座標データなど、いくつかの決められた種類のデータをvertex arrayにすることができる。データの種類と、種類によっては1要素当たりのバイト数を指定して使う。

Buffer object
H/Wが直接アクセス可能なメモリ領域に置くことができるオブジェクト。任意のデータを含めることができる。Vertex arrayを入れることもできる。
Vertex buffer object(VBO)はvertex arrayを入れたbuffer objectのこと。

Vertex array object(VAO)
各vertex arrayの実データの位置(buffer object内でも良い)と状態を保存するオブジェクト。これにより、複数のvertex arrayとbuffer objectを一発で切り替えることが可能になる。OpenGL 3.0以降で標準サポートされているが、それ以前のバージョンの環境やOpenGL ES 1.1の環境でも拡張I/Fとしてサポートされていることが多い。

今回、一旦VAOやVBOを使わずにglVertexPointer()等のvertex arrayのI/Fだけを使ってglVertex3d()やglNormal3d()の嵐を解消したが、いかにも中途半端な感じになった。負荷軽減の目的ならVBOを使わないと意味が無く、VAOが使えるならその方がすっきりするので、結局、頂点座標と法線ベクトルをVBOにしてVAOにまとめた。

glDrawElements()を使うには頂点のインデックスの配列(element array)(vertex arrayの文脈ではindex arrayは色番号の配列のことなので別物) が必要で、どうも頂点のインデックスの配列はVAOに登録できないようなので、glDrawElements()を使うにはglBindVertexArray()するのとは別に、 glBindBuffer()で頂点のインデックスの配列を有効にすることが必要になるようだ(上記ソースコードのdrawSphere()参照)。
なので、座標も法線も全く同じ頂点を使い回さないなら、glDrawElements()でなくglDrawArrays()を使う方がすっきりすると思ったが、連続平面ならポリゴンの各頂点を3回使うのが普通だろうと思い直して、やっぱりVAO+element arrayのbuffer objectという構成にした。

GLUTでOpenGLしてみた

Java3Dには何の不満も無いのだが、Mac OS Xには初めからGLUTが入っていることを知ったので、ちょっと、OpenGLも試しておくことにした。

・ソースコード
polygon_sphere_test1.c

・コンパイル方法の例
Mac OS X 10.7.5で成功したコマンド

gcc -framework OpenGL -framework GLUT polygon_sphere_test1.c

FreeBSD 7.3 + libGL7.4.4 + libglut-7.4.4 で成功したコマンド

gcc -g polygon_sphere_test1.c `pkg-config --cflags --libs glut`

・実行画面

球面の1/8を均等な三角形に分ける方法で球のポリゴンを作成したものである。
マウスでドラッグすると球がその方向に回転し、ボタンを離すと逆方向に少し回転して止まる。
Sキーで、ワイヤーフレーム表示と塗り潰し表示が切り替わる。

回転軸の合成と、ポリゴンの頂点座標の算出に用いる球面線形補間(Slerp: Spherical linear interpolation)には、クォータニオンを使っている。クォータニオンの演算やSlerp等については、参考になる多数のwebページがあるので、説明を省く。GLUTやGLUにはクォータニオンに関する関数が見つからなかったので、適当に作った。特殊なことはしていないと思う。

ドラッグによる回転は、クリックした位置に半径1のトラックボールが接しているような感じに、トラックボールの中心からクリック位置へのベクトル(x,y,-1)とドラッグ先のベクトル(x',y',-1)との外積を回転軸として行っている。カーソル位置に表示されている球面上の座標を求めるのと比べるとかなり手抜きな方法であるが、まあまあそれらしく動いているので良しとする。

描画の度に全ポリゴンの頂点座標を再計算してVertex3dコマンドで転送しており、とても効率が悪い。VAOやVBOを使って頂点データを保存しておくか、せめてGenLists, NewList(GL_COMPILE)を使ってコンパイルしておきたかったが、1つのシンプルな実装例として、一旦公開することにした。