RSS2.0

LWJGL で加算合成、乗算合成ほかを試してみる

OpenGL のアルファブレンドによるいろいろな合成手法をまとめてみました。半透明表示の回ではアルファ値に応じて下地である元の色を強く見せる、一般的なアルファブレンド(透過表示)について扱いましたが、今回はアルファブレンドの他の合成方法について触れてみたいと思います。中でも加算合成、乗算合成はゲームのエフェクトとしてよく使われるものです。

半透明表示の回では色を塗ったポリゴンにアルファブレンドを適用しましたが、同じように、テクスチャーを貼り付けたポリゴンについてもアルファブレンドを適用することができます。今回は不透明な画像、半透明な画像、透過色を持つ透過イメージの3つを、それぞれアルファブレンドで合成してみたいと思います。
8e633b160a92304b7b081a50842bd42c8384.png
なお、私が LWJGL の glColor3f() が好きなので、説明では例にあげる RGB の値の範囲を(0f, 0f, 0f)~(1f, 1f, 1f)としています。一般的な RGB の最大値というと整数の(255, 255, 255)ですが、これも計算結果が 1/255 されるので、0f ~ 1f で指定した場合と結局は同じことになります。

ソースコード

前回までのプロジェクトに test.alphablend というパッケージを作って、その中に以下の2つのクラスを置いてください。

AlphablendPicture.java
package test.alphablend;

import static org.lwjgl.opengl.GL11.GL_BACK;
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_CULL_FACE;
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.glBegin;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glColor4f;
import static org.lwjgl.opengl.GL11.glCullFace;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
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 java.util.ArrayList;
import java.util.List;

import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

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

public class AlphablendPicture {
	private int			width = 270;
	private int			height = 360;
	private int			depth = 10;

	private Texture			textureBackground;
	private List<Texture>	textures;
	private int				textureNumber;
	private int				alphablendNumber;

	public void start() {
		try {
			//	ウインドウを生成する
			Display.setDisplayMode(new DisplayMode(width, height));
			Display.setTitle("Alphablend");
			Display.create();
		} catch(LWJGLException e) {
			e.printStackTrace();
			return;
		}
		
		try {
			//	OpenGL の初期設定
			
			//	テクスチャーを有効化する
			glEnable(GL_TEXTURE_2D);

			//	ポリゴンの表示面を表のみに設定する
			glEnable(GL_CULL_FACE);
			glCullFace(GL_BACK);

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

			//	物体モデル用の設定変更を宣言する
			glMatrixMode(GL_MODELVIEW);
			
			init();
			
			while (!Display.isCloseRequested()) {
				//	オフスクリーンを初期化する
				glClear(GL_COLOR_BUFFER_BIT);
				
				//	キー入力を処理する
				update();
				
				//	オフスクリーンに描画する
				render();

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

	private void init() throws Exception {
		textureNumber = 0;
		textures = new ArrayList<Texture>();
		alphablendNumber = 0;

		//	ファイルのパス指定で画像を読み込む
		textureBackground = new TextureLoader().loadTexture("images/background.png");
		textures.add(new TextureLoader().loadTexture("images/sunflower01.png"));
		textures.add(new TextureLoader().loadTexture("images/sunflower02.png"));
		textures.add(new TextureLoader().loadTexture("images/sunflower03.png"));
	}
	
	private void terminate() {
		if (textureBackground != null) textureBackground.dispose();
		
		for (Texture texture: textures) {
			texture.dispose();
		}
	}

	private void update() {
		while (Keyboard.next()) {

			if ((Keyboard.getEventKey() == Keyboard.KEY_F1) && (Keyboard.getEventKeyState())) {
				textureNumber++;
				textureNumber %= textures.size(); 
			} else if ((Keyboard.getEventKey() == Keyboard.KEY_F2) && (Keyboard.getEventKeyState())) {
				alphablendNumber++;
				alphablendNumber %= AlphaBlend.values().length; 
			} else {
				continue;
			}
			break;
		}
	}

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

		glColor4f(1f, 1f, 1f, 1f);

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

		drawPicture(textureBackground, 0, height);

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

		Texture		texture = textures.get(textureNumber);
		AlphaBlend	alphablend = AlphaBlend.values()[alphablendNumber];

		Display.setTitle(alphablend.name());
		alphablend.config(texture);
		drawPicture(texture, 10, height - 40);
	}

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

		glBegin(GL_QUADS);

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

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

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

		texture.point(0, texture.getHeight());
		glVertex3f(x, y2, 0);
		
		texture.point(texture.getWidth(), texture.getHeight());
		glVertex3f(x2, y2, 0);
		
		glEnd();
	}

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

Alphablend.java
package test.alphablend;

import static org.lwjgl.opengl.GL11.GL_ONE;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_DST_COLOR;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_COLOR;
import static org.lwjgl.opengl.GL11.GL_ZERO;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import test.monolith.v3.Texture;

public enum AlphaBlend {
	AlphaBlend {
		@Override
		public void config(Texture texture) {
			//	アルファ合成
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		}
	},
	Add {
		@Override
		public void config(Texture texture) {
			//	加算合成
			glBlendFunc(GL_SRC_ALPHA, GL_ONE);
		}
	},
	Multiple {
		@Override
		public void config(Texture texture) {
			//	乗算合成
			glBlendFunc(GL_ZERO, GL_SRC_COLOR);
		}
	},
	Screen {
		@Override
		public void config(Texture texture) {
			//	スクリーン合成
			glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE);
		}
	},
	Reverse {
		@Override
		public void config(Texture texture) {
			//	反転合成
			glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
		}
	}
	;
	
	public abstract void config(Texture texture);
}

また、テクスチャーの表示の時に作った以下のファイルも必要になりますので、それぞれ昔のパッケージのままプロジェクト内に残しておいてください。
・test.monolith.v3.TextureLoader
・test.monolith.v3.Texture

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

background.png
background.png
背景となる元の画像です。これに3種類の画像を重ねていきます。

sunflower01.png
sunflower01.png
重ねる画像①、不透明な画像です。

sunflower02.png
sunflower02.png
重ねる画像②、半透明な画像です。画像全体が透過率 50 % で保存されています。

sunflower03.png
sunflower03.png
重ねる画像③、透明イメージです。透明な領域とのふちは綺麗に見せるため、アンチエイリアスで半透明なピクセルになっています。

プログラムの使い方

ソースコードをコピペして画像を置いたら実行してみてくださいー。
F1 キーを押すと重ねる画像が、F2 キーを押すとアルファブレンドの合成方法が切り替わると思います。画像はファイル名の番号順、合成方法は 一般的なアルファブレンド(Alphablend)→加算合成(Add)→乗算合成(Multiple)→スクリーン合成(Screen)→反転合成(Reverse)と切り替わっていきます。

アルファブレンドの係数のおさらい

まずは半透明表示の回のおさらいから。
アルファブレンドでの色の合成結果は、重ねる色と元の色の RGB 値が、それぞれ個別の係数が掛けられた上で足された値となります。
式: 合成結果 = (重ねる色 * 重ねる色の係数) + (元の色 * 元の色の係数)
この式の、重ねる色の係数と元の色の係数を設定するのが、glBlendFunc() になります。glBlendFunc() の第1引数に重ねる色の係数を、第2引数に元の色の係数を渡します。LWJGL では glBlendFunc() に渡す引数は org.lwjgl.opengl.GL11 クラスの static 定数として定義されています。

glBlendFunc() には様々な引数を組み合わせて指定できるため、アルファブレンドの合成方法にはいろいろなものがあります。今回はその中から、よく知られている加算合成、乗算合成、スクリーン合成、反転合成について触れたいと思います。
また、現在の OpenGL では減算合成はサポートされていません。合成結果を求める式は2つの色の足し合わせなので、引き算はできません。OpenGL 拡張には減算合成がありますが、拡張のサポート状況はハードウェアによって異なるようです。

一般的なアルファブレンド

合成結果はこちら。
2c9c07a50ef83048910bd5c04dda7013b6ff.png
重ねる画像のアルファ値がそのまま反映された結果になっています。①番は不透明なまま重ねられ、②番は半透明表示、③番は透過イメージとして表示されています。

LWJGL では以下のように OpenGL の設定をしてやることで、一般的なアルファブレンドを適用することができます。
//	一般的なアルファブレンド
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
重ねる色のアルファ値を考慮して、重ねる色を透過させて合成します。詳細は半透明表示の回を参照してくださいね。

合成結果を求める計算式は、以下のようになります。
式: 合成結果 = (重ねる色 * 重ねる色のアルファ値) + (元の色 * (1 - 重ねる色のアルファ値))
重ねる色のアルファ値が上がっていく(透明になる)ほど、元の色の係数となるアルファ値が下がっていきます(不透明になる)。

加算合成

合成結果はこちら。
d4744601077ab04f2e0831202c799d057ee7.png
画像が重ねられることで、その部分が白に近づいていきます。②は半透明なため①よりも白さが薄まっていて、透過イメージな③では不透明な部分のみきちんと合成されています。
加算合成は発光している様子を表現するのに向いていて、炎や雷といったゲーム特有の光のエフェクトとしてよく使われます。

Java のコードではこうなります。
//	加算合成
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
重ねる色の係数には GL_SRC_ALPHA が指定されていて、重ねるピクセルの透過率がそのまま反映されます。これは一般的なアルファブレンドと同じです。
元の色の係数は GL_ONE で、これは係数として 1 が掛けられることを意味します。元の色が補正されずにそのまま使われるということですね。
式: 合成結果 = (重ねる色 * 重ねる色のアルファ値) + (元の色 * 1)
加算合成は、重ねる色についてはその透過率が考慮されるものの、重ねるピクセルが元の色に純粋に加算されるだけです。
RGBでは色の最大値は白(RGB: 1f, 1f, 1f)なので、明るい色を重ねるほど合成結果は上がっていき、白に近づいていきます。

乗算合成

合成結果はこちら。
2559b3260b385047f0094fb0a9a0f01113c8.png
重ねた画像の部分が暗くなっていています。また、①と②を見比べると透過率が反映されていることもわかります。
また、注意する点として、③の透過イメージで完全に透明な切り抜き部分が黒くなってしまっていることがあります。

LWJGL での設定を見ていきましょう。
//	乗算合成
glBlendFunc(GL_ZERO, GL_SRC_COLOR);

乗算合成は、暗い色を重ねるほどに暗い色になっていく合成方法です。
重ねる色の係数には 0 を意味する GL_ZERO が掛けられ、結果としてこちらは必ず 0 となります。
元の色の係数は GL_SRC_COLOR で、これは重ねる色の RGBA 値を意味します。ここで元の色に重ねる色が掛け合わされていて、これが乗算合成の名前の由来となっています。
式: 合成結果 = (重ねる色 * 0) + (元の色 * 重ねる色)
合成結果は重ねる色、元の色のどちらよりも明るくなることはありません。1f * 0.5f = 0.5f ですし、0.5f * 1f = 0.5f ですね。一方が暗ければその暗さに引きづられて、合成結果は暗くなります。

また、乗算合成では合成結果に重ねる色、元の色の両方の色調が比較的強く影響してきます。一般的なアルファブレンドでは透過率が下がるほど元の色が失われていくので、ここが大きな違いとなります。
e90e9ef6040e904aa80b05b0ddbed0129dad.png
上の図は、青と紺に対して透過率 30% の灰色を合成したものです。左の一般的なアルファブレンドでは2つの色は等しく灰色に近づいていきます(紺のようなはじめから暗い色はむしろ明るくなっていく)が、右の乗算合成では元の色に応じて暗くなる度合いが違っています。乗算合成は暗さの中でも元の色の色調が反映されるため、ゲームのエフェクトとしては影の表現や、透過するメッセージウインドウの背景などで使われています。

乗算合成で重ねる色のアルファ値を反映する

サンプルプログラムの③では、透過部分が黒くなってしまっています。これは重ねる色の透過部分が RGBA(0f, 0f, 0f, 0f) として保存されているため、合成結果も 0 になっているのです(実際に重ねる色を 0 として、式に代入してみてください)。

では、透過イメージをテクスチャとして乗算合成するにはどうすればいいかと言うと、画像の作成時に透明な部分を透明な状態(RGBA: 0f, 0f, 0f, 0f)で保存するのではなく、白色(RGBA: 1f, 1f, 1f, 1f)として保存しておきます。そうすると、元の色に掛ける係数が重ねる色 = 白 = 1f となるので、合成結果は元の色のままとなります。加算合成で係数に GL_ONE を指定するのと同じ理屈ですね。

スクリーン合成

合成結果はこちら。
4e82ac8a0ff36040d40a8430b1913e27d8df.png
色を重ねた部分が白に近づくの点が加算合成に似ていますが、あまり強くは影響してこない感じですね。もうひとつ特色があるので後ほど説明します。

それでは LWJGL の設定内容です。
//	スクリーン合成
glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE);
重ねる色の係数は GL_ONE_MINUS_DST_COLOR で、これは元の色の明るさの反転を意味します。係数は名前の通り、『1 - 元の色』として計算されます。色の最大値は 1f なので、係数が実際にとる値は、元の色が白ならば 0f(黒)に、元の色が黒ならば 1f(白)になります。この係数が重ねる色に適応されるので、元の色が暗いほど重ねる色は明るくなります。
元の色の係数は GL_ONE なので、こちらはそのまま元の色となります。
式: 合成結果 = (重ねる色 * (1 - 元の色)) + (元の色 * 1)
スクリーン合成でも、重ねる色と元の色が加算されますが、重ねる色は元の色が暗いほど明るくなります。
この式は一般的なアルファブレンドの式と似ています。一般的なアルファブレンドではアルファ値に応じて重ねる色の強さが変わりますが、スクリーン合成ではアルファ値だけではなく、元の色の RGBA に応じて重ねる色の強さが変わってきます。サンプルプログラムだとここがちょっとわかりづらいので、背景をモノクロのグラデーションにしたものを用意してみました。
6fe751060c099043a9083fe07e20c87cb3c2.png
こんな感じです。背景が白い上の方ほど、重ねるテクスチャーの色が薄くなっているのがわかります。

スクリーン合成では元の色がそのまま足されるので、加算合成と同じく、合成結果は元の色よりも明るくなります。加算合成では重ねる色もそのまま足されるので、それに比べれば明るくなる度合いは低いですけどね。

なお、透過イメージの透明部分については、乗算合成と違って重ねる色が反転されるので、そのまま(0f, 0f, 0f, 0f)の状態で適用することができます。

反転合成

合成結果はこちら。
1479ceb105cf104bda0ac8b05d52aada03fc.png
名前の通り、RGB の色空間で色が反転している感じです。

LWJGL ではこのような設定をすることで、反転合成が適用できます。
//	反転合成
glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);

反転合成では、明るい色を重ねるほど暗くなっていきます。
重ねる色の係数は GL_ONE_MINUS_DST_COLOR です。スクリーン合成と同じく、元の色が反転されて係数となるので、元の色が暗いほど重ねる色が明るくなります。
元の色の係数は GL_ZERO です。係数が 0 になるので、こちらは常に黒(0f, 0f, 0f)となります。
式: 合成結果 = (重ねる色 * (1 - 元の色)) + (元の色 * 0)
式だけ見るとスクリーン合成と似ていますが、元の色が(必ず 0 になるので)足されないため、補正された重ねる色だけが残る形となります。
性質としてはむしろ乗算合成に近く、色を重ねて暗くしていく合成方法です。乗算合成では暗い色を重ねて暗くしますが、反転合成では明るい色を重ねて暗くします。


これで今回扱っている合成方法についての説明はおわりなのですが、最後にもうひとつ、画像のアルファモードについて触れておきたいと思います。

アルファモード

透過イメージを扱う上で重要となるアルファ値ですが、png や gif と異なり、jpg や bmp はアルファ値を持つことが出来ません。

では半透明な画像を jpg で保存するとどうなるのかというと、私の使っている Photoshop CS4 では強制的に背景色として白が差し込まれ、それに対して画像を半透明で重ねた状態で保存されていました。これは画像本来の色に、あらかじめアルファ値が適用され、(白で)薄められた上で保存されているということになります。
このようなアルファ値の持ち方(jpg はアルファ値を持てないので表現方法というほうがいいのかも)を乗算済みアルファといいます。

それに対して、png のように RGBA としてアルファ値をそのまま持つものを、ストレートアルファといいます。

この2つのアルファ値の持ち方を、画像のアルファモードといいます。アルファモードが乗算済みアルファの画像とストレートアルファの画像とでは、アルファブレンドの適用方法が少し変わってきます。

乗算済みアルファの透過イメージに、一般的なアルファブレンドを適用する

今回使っている画像で、アルファ値を持つものはすべて png なので、アルファモードはストレートアルファのみです。そのため、一般的なアルファブレンドを適用する場合、重ねる色には重ねる色のアルファ値を掛けて補正していました。
式: 合成結果 = (重ねる色 * 重ねる色のアルファ値) + (元の色 * (1 - 重ねる色のアルファ値))
ところが、乗算済みアルファの透過イメージを重ねる場合には、合成結果を求める式は次のようにしなければなりません。
式: 合成結果 = (重ねる色) + (元の色 * (1 - 重ねる色のアルファ値))
アルファモードが乗算済みアルファの場合、重ねる色には既にアルファ値が掛けられた上で保存されているため、アルファブレンド時に再度アルファ値を掛ける必要がないのです。乗算済みアルファが考案された当時はまだハードウェアのグラフィック性能が高くなく、画像作成時に予めアルファ値を適応しておくことで、プログラム実行時の計算数を減らし、高速化を計ったそうです。

サンプルプログラムには含めていませんが、Java のコードにするとこんな感じになります。
if (texture.isAlphaPremultiplied()) {
	//	アルファモードが乗算済みアルファの場合、重ねる色はそのままにしておく
	glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
} else {
	//	アルファモードがストレートアルファの場合、重ねる色にアルファ値をかける
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
テクスチャのアルファモードによって、重ねる色の係数を切り替えてやります。ストレートアルファでは重ねる色の係数を GL__SRC_ALPHA としていますが、乗算済みアルファでは GL_ONE にしています。
論理的には、係数にアルファ値の関係するものが指定されるアルファブレンドはすべて、アルファモードの違いによる影響を受けることになるので注意してください。

アルファモードの調べ方

画像のアルファモードは、java.awt.image.BufferedImage.getType() が java.awt.image.BufferedImage.TYPE_4BYTE_ABGR_PRE を返すかどうかで調べることが出来ます。
if (bufferedImage.getType() != BufferedImage.TYPE_4BYTE_ABGR_PRE) {
	texture.setAlphaPremultiplied(false);
}
PhotochopCS4 では、作成した不透明な png や jpg が BufferedImage.TYPE_4BYTE_ABGR_PRE を返していました。透明な png を乗算済みアルファで保存してみたかったのですが、ちょっとやり方がわかりませんでした。普通に保存すると大抵はストレートアルファになるようなので、ゲーム素材はすべてストレートアルファで保存しておくというルールにしておくのもいいかもしれません。

終わり!!

有名どころのアルファブレンドについて取り上げてみましたが、いかがでしたでしょうか。
glBlendFunc() を使いこなすには画像ファイルのデータ形式なども知っておかなければならないので大変ですが、個人的には、それだけ得られるものも大きいところかなーと思っています。
透過イメージは WEB 上に結構出回っているので想像しやすいですが、加算合成や乗算合成なんかはそれこそペイントソフトでフィルターやレイヤーを本格的に使ったことがないと触る機会はないと思います。私も加算合成というものの存在を知らなかったくちで、市販ゲームのキラキラ輝いてるようなエフェクトはいったいどうやって表現しているんだろう??と不思議がっていました。

さてさて、OpenGL 系のネタはひとまずこれくらいにして、次回からは少し LWJGL の他の機能について触れていこうかなと思いますー。
  LWJGL  コメント (0)  2012/03/23 19:49:14


公開範囲:
前の記事 侵食する黒き翼
次の記事 まるっ!
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2019, 9>>
1234567
891011121314
15161718192021
22232425262728
293012345