RSS2.0

LWJGL でアニメーションさせてみる

LWJGL のタイマーを使ってポリゴンをアニメーションさせてみましたー。いかがでしょうー。
そしてせっかく動かすなら動画にしてみようということで、初動画にチャレンジです。音声はついてないので、セルフサービスであの曲を流していただけると嬉しいです。
動画はファイルサイズの関係などで若干微妙な感じになってしまいましたが、サンプルプログラムのほうはぬるぬる動きます。ぜひぜひ実行してみてください。

ソースコード

それではさっそく今回のサンプルプログラムです。実行して舞い落ちるあの花の様子をお楽しみください。

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

サンプルプログラムのソースコードは、以下の2つになります。

SecretBase.java
package test.secretBase;

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.glClearColor;
import static org.lwjgl.opengl.GL11.glColor4f;
import static org.lwjgl.opengl.GL11.glCullFace;
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.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glRotatef;
import static org.lwjgl.opengl.GL11.glScalef;
import static org.lwjgl.opengl.GL11.glTranslatef;
import static org.lwjgl.opengl.GL11.glVertex3f;
import static test.secretBase.Flower.LIFE_MILLISECOND;

import java.util.ArrayList;
import java.util.List;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
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;
import test.alphablend.AlphaBlend;

public class SecretBase {
	private int				width = 1280;
	private int				height = 720;
	private int				depth = 100;
	private String		   title = "SecretBase";
	
	private long			time;
	private long			timeDelta;
	private long			fps;
	
	private Texture			texture;
	private List<Flower>	flowers;
	private int				   flowerCount;
	private long				 flowerCreatedTime;
	private float				 flowerCreatedDelta;
	private boolean			isReversed;

	public void start() {
		try {
			//	ウインドウを生成する
			Display.setDisplayMode(new DisplayMode(width, height));
			Display.setTitle(title);
			Display.create();
		} catch(LWJGLException e) {
			e.printStackTrace();
			return;
		}
		
		try {
			//	OpenGL の初期設定

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

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

			//	ポリゴンの表示面を表のみに設定する
			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 {
		time = getMillisecond();
		fps = 0;
		
		flowerCount = 300;
		flowers = new ArrayList<Flower>();
		flowerCreatedTime = getMillisecond();
		isReversed = false;
		
		//	ファイルのパス指定で画像を読み込む
		texture = new TextureLoader().loadTexture("images/flower.png");
	}
	
	private void terminate() {
		if (texture != null) texture.dispose();
	}

	private void update() {
		calcFps();
		
		updateFlowers();

		while (Keyboard.next()) {

			if ((Keyboard.getEventKey() == Keyboard.KEY_F1) && (Keyboard.getEventKeyState())) {
				isReversed = !isReversed;
			} else if ((Keyboard.getEventKey() == Keyboard.KEY_F2) && (Keyboard.getEventKeyState())) {
				flowerCount -= 50;
				if (flowerCount < 0) {
					flowerCount = 0;
				}
			} else if ((Keyboard.getEventKey() == Keyboard.KEY_F3) && (Keyboard.getEventKeyState())) {
				flowerCount += 50;
			} else {
				continue;
			}
			break;
		}
		
		//	FPS を調整する
		Display.sync(60);
	}

	private void updateFlowers() {
		int		lowerY = 0 - (texture.getHeight() / 2);
		int		upperY = height + (texture.getHeight() / 2);
		
		for (Flower flower: flowers.toArray(new Flower[] {})) {
			if ((flower.getY() < lowerY) || (upperY < flower.getY())) {
				//	ウィンドウの外に出た花は削除する
				flowers.remove(flower);
			} else {
				//	花の状態を更新する
				flower.update(upperY - lowerY, isReversed);
			}
		}

		//	前フレームからの経過時間から、必要な数だけ花を生成する
		float	createInterval = LIFE_MILLISECOND / flowerCount;
		long	now = getMillisecond();

		flowerCreatedDelta += (now - flowerCreatedTime);
		while (createInterval <= flowerCreatedDelta) {
			if (flowers.size() < flowerCount) {
				int		x = (int)(Math.random() * width);
				int		y = (isReversed)? lowerY: upperY;
				float	scale = random(0.3f, 1f) * random(0.3f, 1f);

				//	オレンジ ~ 赤 ~ 紫の中からランダムで色を作る
				float	rand = random(0f, 1.4f);
				float	r = 1f;
				float	g = (1f < rand)? rand -1f: 0f;
				float	b = (rand <= 1f)? rand: 0f; 
				
				//	ランダムで、色を白に近づける
				rand = random(0.3f, 1f);
				r = r + ((1f - r) * rand);
				g = g + ((1f - g) * rand);
				b = b + ((1f - b) * rand);

				//	ランダムで、色を黒に近づける
				rand = random(0.95f, 1f);
				r *= rand;
				g *= rand;
				b *= rand;

				Flower	flower = new Flower(); 
				
				flower.setPosition(x, y);
				flower.setScale(scale);
				flower.setColor(r, g, b);
				flower.init();
				flowers.add(flower);
			}
			flowerCreatedDelta -= createInterval;
		}
		flowerCreatedTime = now;
	}

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

		//	アルファブレンドで表示する
		AlphaBlend.AlphaBlend.config(texture);

		for (Flower flower: flowers) {
			//	行列スタックに現在の行列を退避させ、新しい行列に対してモデルビュー変換を開始する
			glPushMatrix();

			//	isReversed に応じて花の色を RGB カラーとグレースケールで切り替える
			if (isReversed) {
				glColor4f(flower.getR(), flower.getG(), flower.getB(), 1f);
			} else {
				glColor4f(flower.getBrightness(), flower.getBrightness(), flower.getBrightness(), 1f);
			}

			//	花の中心が原点となるよう座標軸を移動する
			glTranslatef(flower.getX(), flower.getY(), 0);

			//	花毎ののスケールを設定する
			glScalef(flower.getScale(), flower.getScale(), flower.getScale());
			
			//	Z 軸を中心に花を回転させる
			glRotatef(flower.getAngle(), 0, 0, 1);

			drawPicture(texture, texture.getWidth() / -2, texture.getHeight() / 2);
			
			//	行列スタックからもとの行列を取り出す
			glPopMatrix();
		}
	}

	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();
	}
	
	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, " + flowers.size() + " / " + flowerCount + " flowers)");
			fps = 1;
		} else {
			fps++;
		}
	}
	
	private float random(float min, float max) {
		//	min から max までの間で乱数を生成して返す
		float	dist = max - min;
		
		return (float)(Math.random() * dist + min);
	}
	
	public static long getMillisecond() {
		//	現在の時間をミリ秒で返す
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}

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

Flower.java
package test.secretBase;

public class Flower {
	static final float			LIFE_MILLISECOND = 8000f;
	private static final float	ROTATE_MILLISECOND = 5000f;

	private float	x;
	private float	y;
	private float	scale;
	private float	r;
	private float	g;
	private float	b;
	private float	brightness;
	
	private long	time;
	
	private long	start;
	private float	angle;
	
	public void init() {
		start = SecretBase.getMillisecond();
	}

	public void update(int distance, boolean isReversed) {
		if (time <= 0) {
			time = SecretBase.getMillisecond();
			return;
		}
		
		//	前フレームからの経過時間から、このフレームでの移動距離を求める
		long	now = SecretBase.getMillisecond();
		long	delta = now - time;
		float	move = delta / LIFE_MILLISECOND * distance;

		//	この花の生成時間からの経過時間から、回転する角度を求める
		angle = ((now - start) % ROTATE_MILLISECOND / ROTATE_MILLISECOND) * 360f;

		//	isReversed に応じた方向へ移動・回転する
		if (isReversed) {
			y += move;
		} else {
			y -= move;
			angle = 360f - angle;
		}
		
		time = now;
	}

	public float getX() {
		return x;
	}

	public float getY() {
		return y;
	}
	
	public void setPosition(float x, float y) {
		this.x = x;
		this.y = y;
	}

	public float getScale() {
		return scale;
	}

	public float getAngle() {
		return angle;
	}

	public void setColor(float r, float g, float b) {
		this.r = r;
		this.g = g;
		this.b = b;

		//	NTSC 係数による加重平均法でグレースケールに変換後の輝度を求める
		this.brightness = (float)(0.298912 * r + 0.586611 * g + 0.114478 * b);
	}
	
	public float getR() {
		return r;
	}

	public float getG() {
		return g;
	}

	public float getB() {
		return b;
	}

	public float getBrightness() {
		return brightness;
	}

	public void setScale(float scale) {
		this.scale = scale;
	}
	
}

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

flower.png
flower.png
↑白いので注意してください。見えないですね…。

プログラムの使い方

・F1 キー … アニメーションを切り替える
・F2 キー … 表示する花の数を 50 個減らす
・F3 キー … 表示する花の数を 50 個増やす

タイマーの種類と精度

ゲームでは、特にアクションゲームなどではそうなのですが、時間を計る際にはタイマーの精度が重要になってきます。ゲームの性質によって、1 ミリ秒の差を認識できるくらいでいいのか、それともより短い時間を正確に計る必要があるのかが変わってきます。なので、必要な精度を持つタイマーで時間を計らなければなりません。

Java も他のプログラム言語と同様に、タイマーの精度は OS がどれだけの精度をサポートしているかによります。C++ などで開発した Windows 専用のゲームとなれば Windows OS で計れる精度をそのままゲームのタイマーの精度として使うことが出来ます。
しかし Java はクロスプラットフォームなので、Windows で動かす場合と Linux で動かす場合とでタイマーの精度が変わってくる場合があるので注意しなければなりません。Windows でミリ秒まで計れるからといって、他の OS でも必ず同じだけの精度が得られるという保障はないのです。

ここはなかなか難しい問題ではあるのですが、結局のところ、一般的に使えるだけの精度を前提としてゲームを作っていくしかないかなーと思います。どこでも動くというのが Java の魅力なのですが、そもそも OpenGL を使っている時点である程度ハードウェア環境などは制限されてしまいますしね。

一応、こういった背景があるという話を頭の片隅に置いてもらった上で、時間を計る手段について見ていきたいと思います。

java.lang.System.currentTimeMillis()

言わずと知れた Java の標準 API です。協定世界時の 1970 年 1 月 1 日 0 時 0 分 0 秒からの経過時間をミリ秒で返してくれます。
精度はミリ秒ですが、Java のかなり古いバージョンからあるので、十分実用に耐えうるはずです。
基準となる時間が固定されているので、正確に日付時刻までを求められるのも特徴ですね。セーブデータの作成時間など、現実世界の時間を取得するためにお世話になります。
メジャーな API ですし、今回の主旨とは違ってくるので、今回は深く取り上げません。

java.lang.System.nanoTime()

時間をナノ秒単位で計測できるタイマーです。こちらも Java の標準 API ですが、Java 1.5 から導入されたため、古い VM では使えません(とはいってもリリースされたのはだいぶ昔なので、現実的には大抵問題なく使えます)。
ただし、このメソッドは基準時が現実時間のある時点に対応していないため、現実時間を取得することは出来ません。例えばゲームのキャラクターが上に移動していた時間の長さを計るというような、2回の時間測定からその経過時間を計る、というような用途で使うことが出来ます。
また、必ずしもナノ秒の単位を正確に計れるわけではありません。

org.lwjgl.Sys.getTime()

こちらも経過時間を得られるタイマーですが、LWJGL のライブラリによって提供されている API です。java.lang.System.currentTimeMillis() と同じく現実時間の基準時間を持たないため、経過時間の測定にのみ可能となります。
やはり保証される精度は環境によって変わるのですが、同じく LWJGL が提供する org.lwjgl.Sys.getTimerResolution() によって、タイマーの精度を知ることができます。

LWJGL の記事を書いているということもあって、サンプルプログラムではこの org.lwjgl.Sys.getTime() を使っています。
public static long getMillsecond() {
	//	現在の経過時間をミリ秒で返す
	return (Sys.getTime() * 1000) / Sys.getTimerResolution();
}
ナノ秒まで計れるとはいえそこまでの精度は必要ないので、サンプルプログラムではミリ秒を時刻の最小単位としています。
LWJGL でミリ秒を得るには Sys.getTime() の返り値を 1000 倍した上で、Sys.getTimerResolution() で割ることで求められます。

FPS

ゲームではよく時間軸の単位の指標としてフレームというものを使います。1フレームは、アニメーションで花を動かすといった計算処理と、ポリゴンの画面表示をそれぞれ1回ずつ実行することです。サンプルプログラムで言えば、メインループとなる while の中を一回ループしたところで1フレームが終わります。
そしてこのフレームを、1秒間に何フレームを実行できたかを FPS (Frame per second) という数値で表します。

FPS の値はゲーム自体の処理量と、グラフィックカードなどの実行環境によって大きく変わってきます。
特に画像処理はハードウェアの性能に影響されるため、実行時間が一定になることはありません。同じプログラムでも、ハイスペックな PC でなら1秒間に何千個ものポリゴンを表示でき、古い PC ではせいぜい数百個しか表示できない、ということがあります。
また、ゲーム画面に表示するポリゴンの数が大きくなればなるほど画面表示にかかる時間が増えるので、1秒間に処理できるフレーム数は減ってきます。(描画処理に限った話ではなく、衝突判定などの演算処理が遅い場合にも FPS は落ちてしまいます。)

ゲームを開発する時には基準となる FPS を決め、その FPS が常に出していられるように計算アルゴリズムを修正したり、ポリゴン数を調整したりします。FPS が低いとアニメーションがカクカクし過ぎて見るに耐えない状況になりますが、ユーザー全員がハイスペックな PC を持っているわけではないので現実的な目標値を設定しなければなりません。(アニメでぬるぬる動くためには中割りをたくさん描かなければいけないのですが、それだけ動画さんを抱えられるスタジオ経営は以下略)
もちろんどれだけの処理能力を出せるかはハードウェア性能にも影響されるので、どのくらいのスペックのマシンを動作環境にするかも想定する必要があります。

FPS を計算する

実際に FPS を計算してみたいと思います。サンプルプログラムの calcFps() でゲームの FPS を算出しています。
//	現在の時間を求める
long	now = getMillsecond();

//	前のフレームからの経過時間を求め、timeDelta に足す
timeDelta += now - time;
//	次のフレームでの経過時間算出のため、現在の時間を覚えておく
time = now;

//	timeDelta が 1 秒を上回ったら、FPS をリセットする
if (1000 <= timeDelta) {
	timeDelta -= 1000;
	//	ウインドウのタイトルバーに FPS を表示する
	Display.setTitle(title + "(" + fps + "fps, " + flowers.size() + " / " + flowerCount + " flowers)");
	fps = 1;
} else {
	//	FPS を1増やす
	fps++;
}
FPS は1秒間に何フレーム処理できたかを実際に数えていくことで算出できます。
メインループを1周する(FPS が 1 増える)度に前回のフレームからの経過時間を計ります。その経過時間を timeDelta に加算していき、timeDelta の値が 1000 ミリ秒(1秒)を超えた段階で FPS を 1 にリセットします。

サンプルプログラムでは、ついでに LWJGL の Display.setTitle() を使ってウインドウのタイトルバーに FPS を表示させています。FPS は開発中は常に意識しておきたい数値なので、こうしておくと便利です。

FPS を調整する

ところで、処理能力に余裕があると、PC は力の限りメインループをまわし続けることになります。しかし FPS を PC の限界まで出してしまうと CPU やメモリをそのゲームだけで占有してしまうので良くありません。
そこで LWJGL では、FPS の上限を一定値で抑制することのできる org.lwjgl.opengl.Display.sync() が提供されています。
//	FPS を調整する
Display.sync(60);
メインループ内で呼び出している Display.sync() が FPS の上限値を調整してくれます。Display.sync() の引数には FPS の上限値を指定します。
FPS がこの上限値より大きくならないように、LWJGL が必要に応じてスリープ状態になり、うまく FPS を調整してくれます。スリープ状態のプログラムは基本的には CPU を利用しないので、例えばゲームと並行して他のプログラムを実行していたとしても、スリープ中はそちらへの影響を最小限に抑えることができます。
手元の環境でサンプルプログラムを実行してみたところ、Display.sync() なしでは 400 FPS くらいで CPU 使用率 24 % だったものが、Display.sync() で 60 FPS に調整したところ CPU 使用率 2 % くらいに抑えられました。
ただ、上限値といっても厳密に 60 FPS を保証してくれるわけではないので、あくまで上限値の目安として考えてください。

ポリゴンをアニメーションさせる

サンプルプログラムでは、花を上下に移動させるのと、花を回転させるのにタイマーを使っています。花の移動がわかりやすいと思うので、まずはそちらについて見ていきましょう。

花の移動は、フレーム毎に移動距離を求め、その量に応じて花の座標を変更してやります。移動方向を変える場合に備え、常に現在位置から移動させていくというやり方にしています。
移動距離を求めるのに、FPS 算出と同様に、タイマーによる経過時間の測定を行っています。
//	1度目の時間測定をする
if (time <= 0) {
time = SecretBase.getMillsecond();
	return;
}

//	2度目の時間測定をする
long	now = SecretBase.getMillsecond();

//	経過時間を計算する
long	delta = now - time;

//	delta の大きさに応じてポリゴンの移動距離を計算する
float	move = delta / LIFE_MILLSECOND * distance;

//	次回の処理のために、基準となる時間(1度目の測定時間)を現在の時間とする
time = now;
全体の移動距離(ウインドウの上から下まで。変数 distance 値に格納されている)とそれを移動するのにかかる時間(定数 LIFE_MILLSECOND)から、フレーム毎の経過時間に応じた実際の移動距離を割り出しています。
経過時間に応じてフレーム毎の移動距離を変えているので、例え FPS が大きく変わったとしても、アニメーションが終わるまでの時間は(ほぼ)変わりません。フレーム数が減ってもカクカクするだけでゲームの進行速度は変わらないようにしています。

一方でこの方法ではフレーム毎の移動距離が変動するため、フレーム数が少なすぎるとワープしているように見えてしまいます。回避するには、逆にフレーム毎の移動距離を固定にし、アニメーション全体の時間を変えるようにする必要があります。この2つは正反対のやり方なので、用途に応じて使い分けてみてください。

サンプルプログラムのその他のポイント

ひとまずタイマーを使ったアニメーションの説明はこれですべてになります。
なのですが、今回のサンプルプログラムのその他のコードについて、いくつか触れておきたいと思います。

Z 軸を中心にポリゴンを回転させる

花の回転は、ポリゴンの中央部分を中心に時計回り(もしくは反時計回り)に回転させています。ポリゴンの回転の回で X 軸、Y 軸を中心としたモデルの回転を行いましたが、あれと同じことを Z 軸を回転軸に行えば、XY 平面と並行に回転させることができます。
for (Flower flower: flowers) {
	//	モデルビュー変換中の行列を、行列スタックに退避させる
	glPushMatrix();

	...

	//	座標の中心を花の中央に移動する
	glTranslatef(flower.getX(), flower.getY(), 0);

	...
			
	//	Z 軸を中心にポリゴンを回転させる
	glRotatef(flower.getAngle(), 0, 0, 1);
	
	//	花のポリゴンを描画する
	drawPicture(texture, texture.getWidth() / -2, texture.getHeight() / 2);

	//	退避させていた元のモデルビュー変換の行列を、行列スタックから取り出す
	glPopMatrix();
}
ポイントは、ひとつひとつの花ごとに、その花の中央部分を中心として回転させなければならないという点です。座標系の原点座標を、毎回回転したい花の真ん中にもって来る必要があるのです。
そこで glPushMatrix() / glPopMatrix() を使い、各花の描画処理毎にモデルビュー変換する行列の状態をリセットしています。glPushMatrix() を呼び出すと、現在変換している行列はいったん行列スタックに退避され、変換する行列は新たに用意された初期状態のものに切り替わります(そして行列スタックに退避されたもとの行列は、glPopMatrix() を呼び出すまで退避されたままとなります)。このまっさらな新しい行列に対して移動と回転を行うことで、1つの花に対するモデルビュー変換が他の花のモデルビュー変換に干渉することを防いでいます。
基本はポリゴンの回転の回と同じですので、必要であればこちらも読み返してみてください。

ポリゴンの色をランダムに補正する

サンプルプログラムでは、ひとつひとつ色を変えて花のテクスチャーを表示しています。テクスチャーの画像自体は白い透過イメージですが、これをテクスチャーマッピングする際に glColor4f() で個別の色をつけています。
花の色は、オレンジ~赤~赤寄りの紫の範囲でランダムに設定しています。以下がその色の設定部分です。
//	オレンジ ~ 赤 ~ 紫の中からランダムで色を作る
float	rand = random(0f, 1.4f);
float	r = 1f;
float	g = (1f < rand)? rand -1f: 0f;
float	b = (rand <= 1f)? rand: 0f; 

//	ランダムで、色を白に近づける
rand = random(0.3f, 1f);
r = r + ((1f - r) * rand);
g = g + ((1f - g) * rand);
b = b + ((1f - b) * rand);

//	ランダムで、色を黒に近づける
rand = random(0.95f, 1f);
r *= rand;
g *= rand;
b *= rand;

Flower	flower = new Flower(); 

flower.setColor(r, g, b);
色の設定はまず基本となる色(色相)を作り、さらにその明度と彩度をランダムに調整しています。
色相の生成では、r: 1f に対して g, b 値をどの程度上げるかを乱数で決めています。random() はサンプルプログラムで実装したユーティリティメソッドで、第1引数から第2引数の間で乱数を返します。
次に、作った色相に別の乱数を掛けることで明度と彩度を調整しています。白(1f, 1f, 1f)に近付けることで彩度を落とし、黒(0f, 0f, 0f)に近付けることで明度を落としています。なぜこのような調整をしているかといえば原作の色合いに近づけたかったからというだけで、これが明度や彩度の正しいいじり方という訳ではありません。

色情報をグレースケールに変換する

サンプルプログラムを動かしてもらえばわかる通り、花の表示はモノクロのグレースケールでも行っています。花には RGB カラーを持たせているので、グレースケール表示の際には RGB カラーからの変換を行っています。それが以下のコードです。
//	NTSC 係数による加重平均法でグレースケールに変換後の輝度を求める
this.brightness = (float)(0.298912 * r + 0.586611 * g + 0.114478 * b);
グレースケールへの変換手法にはいくつかあるのですが、今回は NTSC 係数による加重平均法を用いています。この手法では変換する色の RGB 値にそれぞれ一定の係数を掛けた上で足し合わせ、グレースケールの輝度としています。RGB によって輝度の割合(係数)が違うのは、色相によって明るく感じられる色と暗く感じられる色があるのを反映したものです。

終わり。

前回までは OpenGL にフォーカスした内容でしたが、今回はゲームの演出には欠かせないアニメーションについて取り上げてみました。いろいろ動かしまくってみると、やはり OpenGL のレンダリング性能はすごいなーと感じます。
2D ゲームくらいなら pure Java で、awt でもいけそうかなと昔チャレンジしたことがあるのですが、それなりの FPS を出そうと思ったり、アルファブレンドを使おうとするとどうしても処理落ちしてしまうため、挫折した経験があります。最近の Java では awt でも使えるのであればハードウェアアクセラレーションを使えるようになったとか聞いていますが、どの程度速くなってるんでしょー。

それはそうと、今回はサンプルプログラムのアニメーションを撮りたくて動画作成にチャレンジしてしまいました。キャプチャしてファイル形式を変換しただけですが、自分で素材(?)から用意して作ったりで結構楽しかったです(笑
サイズを気にしなければすごい高画質にもできるのでしょうが、さすがそういう訳にもいかず。一度世の職人さん達の圧縮技術を見てみたいです。
  LWJGL  コメント (1)  2012/03/28 19:48:39


公開範囲:
2015/04/05 16:21:49   momokan 公開範囲: すべて, 承認済み
花の画像が切れていたので貼り付けなおしました!
ご指摘いただいた方、ありがとうございましたー!
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2019, 5>>
2829301234
567891011
12131415161718
19202122232425
2627282930311