RSS2.0

LWJGL でデプステストを試してみる

今回は隠面消去の手法の1つである、デプステスト(深度テスト)について扱ってみたいと思います。
隠面消去とは、複数のテクスチャーを重ね合わせて描き込む際に、背後にある方のテクスチャーを割り出して手前のテクスチャーで隠すことです。

普通、複数のテクスチャーを重ね合わせる場合、先に描いたテクスチャを後から描いたテクスチャが上書きするため、奥にあるテクスチャーから描きこんでいく必要があります。しかし隠面消去を用いると、テクスチャーの描き込む順序をプログラマーが考慮しなくてよくなるため、コード側でテクスチャーを表示順でソートするといった処理が不要になるというメリットがあります。

デプステストによる隠面消去で、背景、キャラクター、キャラクターの前後にある複数のカードを重ねてみました。
screenShot.png
キャラクターの前後にあるカードがそれぞれ適切に上書きしている・されているのがわかると思います。
カードとキャラクターは透過イメージになっていて、これはアルファブレンドで合成されています。デプステストには透過イメージを扱いきれないという問題があるのですが、今回はこの問題への解決策についても触れていきます。

ソースコード

サンプルプログラムのソースコードです。
実行には昔使った以下のファイルが必要になりますので、いつも通りプロジェクトに配置してください。
test.monolith.v3.TextureLoader
test.monolith.v3.Texture
test.alphablend.AlphaBlend

今回のサンプルプログラムのソースコードは、以下の 1 つのみです。
DepthTest.java
package test.depth;

import static org.lwjgl.opengl.GL11.GL_ALPHA_TEST;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_GREATER;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glAlphaFunc;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearColor;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glGetInteger;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glVertex3f;
import static org.lwjgl.opengl.GL13.GL_MULTISAMPLE;
import static org.lwjgl.opengl.GL13.GL_SAMPLES;
import static org.lwjgl.opengl.GL13.GL_SAMPLE_ALPHA_TO_COVERAGE;
import static org.lwjgl.opengl.GL13.GL_SAMPLE_BUFFERS;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.PixelFormat;

import test.alphablend.AlphaBlend;
import test.monolith.v3.Texture;
import test.monolith.v3.TextureLoader;

public class DepthTest {
	private int			width = 900;
	private int			height = 600;
	private int			depth = 300;
	private String		title = "depth test";

	private long		time;
	private long		timeDelta;
	private long		fps;

	private Texture		textureBackground;
	private Texture		textureCard;
	private Texture		textureElizabeth;

	private float		cardAngle = 0;

	public void start() {
		try {
			//	ウインドウを生成する
			Display.setDisplayMode(new DisplayMode(width, height));
			Display.setTitle(title);
			Display.create(new PixelFormat(0, 8, 0, 4));
		} catch(LWJGLException e) {
			e.printStackTrace();
			return;
		}

		try {
			//	OpenGL の初期設定
			
			//	オフスクリーンを初期化する際の背景色を指定する
			glClearColor(0f, 0f, 0f, 0f);
			
			//	テクスチャーを有効化する
			glEnable(GL_TEXTURE_2D);

			//	アルファブレンドを有効化する
			glEnable(GL_BLEND);

			//	カメラ用の設定変更を宣言する
			glMatrixMode(GL_PROJECTION);
			//	設定を初期化する
			glLoadIdentity();
			//	視体積(目に見える範囲)を定義する
			glOrtho(0, width, 0, height, -depth / 2, depth / 2);

			//	物体モデル用の設定変更を宣言する
			glMatrixMode(GL_MODELVIEW);

			glEnable(GL_DEPTH_TEST);

			if ((1 <= glGetInteger(GL_SAMPLE_BUFFERS)) && (2 <= glGetInteger(GL_SAMPLES))) {
				//	マルチサンプリングを有効にする
				glEnable(GL_MULTISAMPLE);
				//	Alpha to coverage を有効にする
				glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE);
			} else {
				//	アルファテストを有効にする
				glEnable(GL_ALPHA_TEST);
				//	アルファテストの条件を設定する
				glAlphaFunc(GL_GREATER, 0.7f);
			}
			
			init();

			while (!Display.isCloseRequested()) {
				//	オフスクリーンを初期化する
				glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

				//	FPS を計算する
				calcFps();

				//	キー入力を反映する
				update();

				//	オフスクリーンに描画する
				render();

				//	オフスクリーンをスクリーンに反映する
				Display.update();
			}
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			terminate();
			Display.destroy();
		}
	}

	private void init() throws Exception {
		time = getMillisecond();
		fps = 0;

		//	ファイルのパス指定で画像を読み込む
		textureBackground = new TextureLoader().loadTexture("images/velvetRoom.png");
		textureElizabeth = new TextureLoader().loadTexture("images/elizabeth.png");
		textureCard = new TextureLoader().loadTexture("images/personaCard.png");
	}

	private void terminate() {
		if (textureBackground != null) textureBackground.dispose();
		if (textureElizabeth != null) textureElizabeth.dispose();
		if (textureCard != null) textureCard.dispose();
	}

	private void update() {
		cardAngle = (cardAngle + 1) % 360;

		//	FPS を調整する
		Display.sync(60);
	}

	private void render() {
		//	設定を初期化する
		glLoadIdentity();

		AlphaBlend.AlphaBlend.config(textureCard);
		drawPicture(textureBackground, 0, height, -(depth / 2) + 1);
		drawPicture(textureElizabeth, 70, height, 0);
		
		int	count = 12;

		for (int i = 0; i < count; i++) {
			float	angle = (cardAngle + 360 / count * i) % 360;
			float	x = (float)(Math.cos(Math.toRadians(angle)) * 300);
			float	y = (float)(Math.sin(Math.toRadians(angle)) * -100);
			float	z = (float)(Math.sin(Math.toRadians(angle)) * 100);
			
			drawPicture(textureCard, (width - textureCard.getWidth()) / 2 + x, 300 + y, z);
		}
	}

	private void drawPicture(Texture texture, float x, float y, float z) {
		//	テクスチャをバインドする
		texture.bind();

		glBegin(GL_QUADS);

		float	x2 = x + texture.getWidth();
		float	y2 = y - texture.getHeight();

		texture.point(texture.getWidth(), 0);
		glVertex3f(x2, y, z);

		texture.point(0, 0);
		glVertex3f(x, y, z);

		texture.point(0, texture.getHeight());
		glVertex3f(x, y2, z);

		texture.point(texture.getWidth(), texture.getHeight());
		glVertex3f(x2, y2, z);

		glEnd();
	}

	private void calcFps() {
		long	now = getMillisecond();

		//	前のフレームからの経過時間を求め、timeDelta に足す
		timeDelta += now - time;
		time = now;

		if (1000 <= timeDelta) {
			//	timeDelta が 1 秒を上回ったら、FPS をリセットする
			timeDelta -= 1000;
			Display.setTitle(title + "(" + fps + "fps)");
			fps = 1;
		} else {
			fps++;
		}
	}

	public static long getMillisecond() {
		//	現在の時間をミリ秒で返す
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}

	public static void main(String[] args) {
		new DepthTest().start();
	}

}

こちらが使用する画像です。プロジェクトのディレクトリ直下に images というディレクトリを用意して、その中に指定の名前で置いてください。

velvetRoom.png
velvetRoom.png

elizabeth.png
elizabeth.png

personaCard.png
personaCard.png

デプステスト

デプステストは後から描き込むテクスチャの Z 座標が先に描き込まれたテクスチャの Z 座標より大きいかを判定し、テクスチャを描き込むかどうかを判定する手法です。

デプステストを有効にするには、以下のような設定を行います。
//	デプステストを有効化する
glEnable(GL_DEPTH_TEST);

また、オフスクリーンを初期化する際には、デプスバッファと呼ばれる、デプステスト用の記憶領域も初期化する必要があります。
//	オフスクリーンを初期化する
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClear() メソッドに引数に、GL_DEPTH_BUFFER_BIT も指定(論理和を求めてビットを立てる)してください。

デプスバッファは別名 Z バッファとも呼ばれ、描き込まれたテクスチャーの z 座標値(深度)を保持しておく記憶領域です。次にテクスチャーを上書きする際には、上書きするテクスチャーの z 座標値と、既に描き込まれているテクスチャーの z 座標値(= デプスバッファの値)を比較し、より手前にあるか(上書きすべきか)を判定します。

デプステストでの透過イメージを描き込む際の制限事項

デプステストはテクスチャーを上書きするかしないかを判定する機能なので、上書きすると判定された場合には、もちろんアルファブレンドによって透過イメージも綺麗に合成できます。
しかし、アルファブレンドの実装上、まだ描きこまれていないテクスチャに対しては合成ができないため、背後にあるはずのテクスチャーが透過されない場合があります。

以下の順序でテクスチャーを描き込むとします。
1. 背景(z: -100)テクスチャを描く
2. キャラクター(z: 0)テクスチャを描く
3. カード(z: -10)テクスチャを描く

2 でキャラクターを描き込む際に、まずデプステストによって z 座標がチェックされ、既に 1 で描きこまれている背景よりも手前にあるキャラクターが上書きされることになります。
次にアルファブレンドにより、背景と上書きするキャラクターが合成されます。
depthtest1.png

3 でカードを描き込む際には、デプステストによって z 座標がチェックされ、既に描きこまれているキャラクターよりも奥にあるキャラクターは上書きされないことになります。
ここで、カードに重なっているキャラクターの部分が半透明だった場合、上にあるキャラクターのテクスチャーが透過してその奥にあるカードも表示されてほしいところですが、デプステストによってカードは描きこまれないと判定されている上に、キャラクターの背景に対するアルファブレンドは既に手順 2 で終わってしまっているため、2つの合成結果は今となっては変えようがありません。
depthtest2.png
サンプルプログラムのキャラクターの画像は、フキダシの下(上の画像では左上)部分が透過色となっていますが、その背後にはカードが回り込んでくれません。
また、カード自体も縁が透過色となっているため、カード同士が重なりあう場合にも、背景しか見えない領域ができてしまっています。

奥にあるテクスチャーから描き込むようにすればこの問題は起きないのですが、それではそもそもテクスチャーの書き込み順を考えなくて済むという隠面消去のメリットがなくなってしまいます。これが、デプステストでの透過イメージを描き込む際の制限事項となります。

この問題の解決策としては、アルファテストを併用する方法と、Alpha to Coverage を併用する方法があります。

アルファテストを使って透過イメージを表示する

アルファテストは、テクスチャーのアルファ値に応じてテクスチャー自体を切り抜いてしまう技術です。例えば、アルファテストの条件として『アルファ値が 0.7f より大きい』ことを設定した場合、テクスチャーの各ピクセルのうち、アルファ値が 0.7f 以下のピクセルは描きこまれなくなります。(アルファテストはデプステストより先に評価されます。)
//	アルファテストを有効にする
glEnable(GL_ALPHA_TEST);
//	アルファテストの条件を設定する
glAlphaFunc(GL_GREATER, 0.7f);
アルファテストを有効にするには、glEnable() の引数に GL_ALPHA_TEST を渡します。
また、アルファテストの表示条件は glAlphaFunc() メソッドの引数として指定します。第一引数には比較に用いる等号・不等号を、第二引数には閾値を渡します。上記のコードではアルファ値が 0.7f より大きいピクセルのみテクスチャーとして書き込まれることになります。

描き込まれなかったピクセルは、その部分にはテクスチャーがなかったものとして扱われるので、さらにテクスチャーを上書きしようとした際にも、もともとの背景画像と次に上書きするテクスチャーとでデプステストが評価されることになります。

アルファテストでは半透明は再現できない

アルファテストはテクスチャー内のピクセルを表示するか・しないかを判定する機能なので、アルファブレンドのように半透明でピクセルを合成する、ということはできません。そのため、アルファテストによって切り抜かれたテクスチャーにはシャギーができてしまいます。
alphaBlend.png
alphaTest.png
上が通常のアルファブレンド、下がアルファテストを使ってテクスチャーを重ねた場合です。
デプステストでアルファテストを併用することで透明な画像も表示できるようになりますが、半透明な画像(ピクセル)については相変わらず制限事項となってしまいます。

Alpha to Coverage を使って透過イメージを表示する

Alpha to Coverage は、もともとはアンチエイリアスを行うための技術なのですが、デプステストで併用することで、デプステストにおける透過イメージの問題を解決してくれます。アンチエイリアスをかける際のマスクとしてテクスチャーの持つアルファ値を使うというもので、ビデオカードのマルチサンプリング機能をベースに実現されています。
alphaToCoverage.png
アンチエイリアスを目的とした機能だけあって、デプステストを使っていても半透明のピクセルを上書きすることができます。
なお、この機能は OpenGL 1.3 からさポートされています。

ビデオカードの設定を確認する

Alpha to Coverage を利用するには、ビデオカードのアンチエイリアスを適切に設定しておく必要があります。
Winodws 環境で nVidia のビデオカードを使っている場合、コントロールパネルにある NVIDIA コントロールパネルから『3D 設定の管理』を表示し、『アンチエイリアシング - モード』と『アンチエイリアシング - 設定』が "アプリケーションによるコントロール" に設定されていることを確認してください。
手元の環境では、初期設定が "アプリケーションによるコントロール" になっていたので、おそらく大抵の環境で設定変更は不要になると思います。
videoCardSetting2.png
"アプリケーションによるコントロール" を設定すると、プログラム側でアンチエリアスで使用するマルチサンプリングのバッファ数を(ビデオカードのスペックの範囲で)必要な数に設定することができます。この数値が高くなるほど、処理は重くなりますが綺麗なアンチエイリアスを実現できます。

ちなみに『アンチエイリアシング - モード』を "アプリケーション設定の変更" にすると、『アンチエイリアシング - 設定』でマルチサンプリングのバッファ数を固定値に指定することができます。
ただし、これはハードウェア(ビデオカード)レベルの設定なので、すべてのプログラムに対して有効となります。その結果、OpenGL やサンプルバッファを使ったアンチエイリアスを使用しないアプリケーションにも同じだけのリソースが割り当てられてしまいます。必要なアプリケーションだけに必要な数を使えるように、"アプリケーションによるコントロール" に設定しておきましょう。

なお、『アンチエイリアシング - 設定』で設定できる最大バッファ数がハードウェアの限界となるので、プログラム側でもそれ以上の量を要求することはできません。

RAREON のビデオカードを使っている場合にも、同じく設定ウィンドウにてアンチエイリアシングのレベルを設定できるそうです。こちらも初期設定は "アプリケーション設定を使用する" が有効となっているようなので、プログラム側で必要な量のバッファ数を要求できるようです。
http://d.hatena.ne.jp/penciler/20090413/1239630613

Alpha to Coverage を実装する

Alpha to Coverage で使うマルチサンプリングのバッファ数は、LWJGL の Display 生成時に PixcelFormat インスタンスの引数として指定します。
//	ウインドウを生成する
Display.setDisplayMode(new DisplayMode(width, height));
...
//	マルチサンプリングのバッファ数を 4 に指定して、ウインドウを生成する
Display.create(new PixelFormat(0, 8, 0, 4));
org.lwjgl.opengl.Display.create() メソッドには org.lwjgl.opengl.PixelFormat.PixelFormat() インスタンスを渡すことができるのですが、この PixelFormat のコンストラクタの第四引数がマルチサンプリングのバッファ数となります。
サンプルプログラムでは 4 を指定してみました。

第一引数から第三引数までは、デフォルト(引数なしの PixelFormat() コンストラクタで指定される)値を指定しています。これらの引数が何なのかは、ここでは触れません。


次に、マルチサンプリング機能と Alpha to Coverage を有効化します。
if ((1 <= glGetInteger(GL_SAMPLE_BUFFERS)) && (2 <= glGetInteger(GL_SAMPLES))) {
	//	マルチサンプリングを有効にする
	glEnable(GL_MULTISAMPLE);
	//	Alpha to Coverage を有効にする
	glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE);
}
マルチサンプリングを有効にするには org.lwjgl.opengl.GL13.GL_MULTISAMPLE を、Alpha to Coverage を有効にするには org.lwjgl.opengl.GL13.GL_SAMPLE_ALPHA_TO_COVERAGE を、それぞれ glEnable() の引数に渡して有効にします。

また、この機能を使うには実行環境が以下の条件を満たしている必要があります。
・glGetInteger(GL_SAMPLE_BUFFERS) が 1 以上を返す
・glGetInteger(GL_SAMPLES) が 2 以上を返す
glGetInteger() は、引数に渡したパラメーターの、実行環境での値を調べるメソッドです。
GL_SAMPLE_BUFFERS はマルチサンプリングが使えるかどうかを示すパラメーターで、使えれば 1 を、使えなければ 0 を返します。
GL_SAMPLES はマルチサンプリングで利用されるサンプルバッファの数を示すパラメーターです。これが先ほどビデオカードで設定していたバッファ数に対応しています。

まとめ

デプステストによる隠面消去はとても便利な機能ですが、透過イメージの問題への対応が少しやっかいです。
綺麗に合成できる Multisample Transparency を使いたいところですが、手元の Windows 環境でもビデオカードの設定を変更しないと有効にはなりませんでした。設定変更には一手間かかるので、デフォルトではアルファテストによる対応としておき、Multisample Transparency が使える環境では Multisample Transparency を使うという実装にしておくと良さそうですね。

補足: 過去の記事で表示した立方体の隠面消去について

隠面消去を行わない場合、テクスチャーの z 座標値に関わらず、後から描いたテクスチャーが手前に表示されますが、それでは過去の Hello world(環境構築編)から立方体に画像を貼り付ける回で作っていた立方体ではどう隠面消去していたのかについて補足しておきたいと思います。立方体を回転させる回以降は立方体を回転させて裏側を手前に持ってきたりということをしてましたが、常に見えているのは立方体の手前の面でした。ソースコードを見直してもらえればわかりますが、各面の描画順序は常に同じなのに、手前に見えている面はその時々で手前にあるべき面なのでした。

では初回から実は隠面消去をしていたのかというとそんなことはなく、代わりにやっていたのはテクスチャーの片面表示です。
//	ポリゴンの表示面を表のみに設定する
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
立方体の面はすべて外側を向くように配置しておき、かつ表を向いているテクスチャーしか表示しないようにしていたため、立方体の奥の面は(こちらにはテクスチャーの裏が向いているので)表示されていなかった、ということになります。

立方体には手前の面と奥の面の2層しかないという特別な状況だからこそできた手抜き実装ですね。
”ポリゴンの扱いに慣れるまでは練習の意味でも表のみ表示するように設定しておくといいかもしれません。”とかしれっと書いておいて、面倒な隠面消去については先送りにしていたのでした。ちゃんちゃん。

  LWJGL  コメント (0)  2012/05/15 15:54:46


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2019, 9>>
1234567
891011121314
15161718192021
22232425262728
293012345