RSS2.0

LWJGL で光源を使ってみる

LWJGL でのライティングについて試してみました。3D モデルといっしょに光源を置いて光を当て、3D モデルに陰影をつけていく機能です。
screenshot.png
光源から発せられる光には、モデルの照らし方によっていくつか種類があります。
またライティングにおいては、光をどの向きに反射するかを決める法線をモデルに設定していく必要があります。

ベクトルなんかも出てきてちょっと長くなるかもしれませんが、それでは見ていきましょー。

lwjgl_util.jar を準備する

法線を計算するのにベクトルを使うのですが、LWJGL には x, y, z のフィールドを持ったベクトル用のクラス: org.lwjgl.util.vector.Vector3f が用意されています。ベクトルの和や差、外積なんかを計算してくれるメソッドを持っているのでかなり便利です(自分で実装してもいいですが、せっかくあるものなので使って行きたいと思います)。

Vector3f クラスは lwjgl.jar ではなく、lwjgl_util.jar に入っています。公式サイトで公開されているアーカイブに含まれているので、Maven を使っていない方は自分で classpath に通してください。

Maven プロジェクトの設定

Maven を使っている方は、lwjgl.jar と同じく Maven を使ってダウンロード・classpath への追加をすることができます。
LWJGL の環境構築編の時のように、Maven プロジェクトのディレクトリにある pom.xml を開き、lwjgl.jar の設定の下に lwjgl_util.jar の設定を追加してください。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
  <dependencies>

	<!-- LWJGL ver2.8.3 の設定 -->
	<dependency>
	  <groupId>org.lwjgl.lwjgl</groupId>
	  <artifactId>lwjgl</artifactId>
	  <version>2.8.3</version>
	</dependency>
	<dependency>
	  <groupId>org.lwjgl.lwjgl</groupId>
	  <artifactId>lwjgl_util</artifactId>
	  <version>2.8.3</version>
	</dependency>
	<!-- LWJGL ver2.8.3 の設定 -->
 
  </dependencies>
...  
</project>
2012/1/23 に LWJGL の ver2.8.3 が公開されていたのですね。LWJGL の環境構築編では 2.8.2 を使っていたので、今回は最新バージョンを使うように、lwjgl.jar の方も <version> タグを修正してみました。
lwjgl_util.jar は lwjgl.jar に依存してますので、2 つの jar のバージョンは必ず揃えてください。

pom.xml を修正したら、コマンドラインから Maven プロジェクトを Eclipse 用に設定します。
Maven プロジェクトのディレクトリ(ここでは C:\eclipse\workspace\test としています)に移動し、以下のようにコマンドを実行してください。
E:> cd C:\eclipse\workspace\test
C:\eclipse\workspace\test> mvn nativedependencies:copy
C:\eclipse\workspace\test> mvn clean eclipse:eclipse -DdownloadSources=true -DdownloadJavadocs=true
LWJGL のバージョンを変更したら、mvn nativedependencies:copy を忘れずに実行しないと、Java で読み込む JNI ライブラリのバージョンが合わずに、以下のようなエラーが発生しますので注意してください。
Exception in thread "main" java.lang.LinkageError: Version mismatch: jar version is '23', native library version is '24'
	at org.lwjgl.Sys.<clinit>(Sys.java:118)
	at org.lwjgl.opengl.Display.<clinit>(Display.java:135)
	at test.light.Lighting.start(Lighting.java:110)
	at test.light.Lighting.main(Lighting.java:514)

LWJGL の jar がダウンロードされれば、BUILD SUCCESSFUL と表示されます。

 Eclipse のプロジェクトの更新

最後に、Eclipse を起動し、画面左側の Package Explore タブで昔インポートした Maven プロジェクトを右クリックし、Refresh メニューを押してください。これで Eclipse にも最新の classpath と依存している jar ライブラリが読み込まれます。

ソースコード

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

今回のサンプルプログラムのソースコードは、以下の 2 つです。

Lighting.java
package test.light;

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_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
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.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.glNormal3f;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glRotatef;
import static org.lwjgl.opengl.GL11.glTranslatef;
import static org.lwjgl.opengl.GL11.glVertex3f;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.util.vector.Vector3f;

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

public class Lighting {
	private static final int[]		rs = {
		12, 40, 58, 70, 78, 85, 90, 95, 100, 104, 108, 112, 116, 118, 120, 122, 123, 124, 125,
		122, 119, 116, 110, 103, 90, 75, 28
	};

	private int			width = 800;
	private int			height = 500;
	private int			depth = 800;
	private String		title = "lighting";

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

	private Texture			texture;
	private LightManager	lightManager;

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

		try {
			//	OpenGL の初期設定
			
			//	オフスクリーンを初期化する際の背景色を指定する
			glClearColor(0.5f, 0.5f, 0.5f, 1f);
			
			//	アルファブレンドを有効化する
			glEnable(GL_BLEND);
			
			//	カメラ用の設定変更を宣言する
			glMatrixMode(GL_PROJECTION);
			//	設定を初期化する
			glLoadIdentity();
			//	視体積(目に見える範囲)を定義する
			glOrtho(0, width, 0, height, -depth / 2, depth / 2);

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

			//	デプステストを有効にする
			glEnable(GL_DEPTH_TEST);

			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;

		//	光源を設定する
		lightManager = new LightManager();
		lightManager.initLight();
		
		//	ファイルのパス指定で画像を読み込む
		texture = new TextureLoader().loadTexture("images/kunlun.png");
	}

	private void terminate() {
		if (texture != null) texture.dispose();
	}

	private void update() {
		lightManager.update();

		//	タイトルバーを更新する
		Display.setTitle(String.format("%s(%dfps) %s", title, calcedFps, lightManager.getLabel()));

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

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

		//	光源を設定する
		lightManager.light();

		//	画面中央の座標を (0, 0, 0) とするよう座標系を移動する
		glTranslatef(width / 2f, height / 2f, 0);
		
		glRotatef(lightManager.getXAngle(), 0, 1, 0);	//	y 軸を中心に xAngle 度回転させる
		glRotatef(lightManager.getYAngle(), 1, 0, 0);	//	x 軸を中心に yAngle 度回転させる

		//	モデルを表示する
		drawKunlun();
	}
	
	private void drawKunlun() {
		float	height	= 360f;
		float	yOffset = height / 2;
		
		texture.bind();
		AlphaBlend.AlphaBlend.config(texture);
		
		glEnable(GL_TEXTURE_2D);
		glColor4f(1f, 1f, 1f, 1f);
		glBegin(GL_TRIANGLES);

		//	y 軸方向に面を描いていく
		for (int yN = 0; yN < rs.length - 1; yN++) {
			float	y = (height / rs.length * yN);
			float	y2 = (height / rs.length * (yN + 1));
			float	topY = yOffset - y;
			float	bottomY = yOffset - y2;

			drawWalls(rs[yN], rs[yN + 1], topY, bottomY, (int)y + 20, (int)y2 + 20);
		}
		//	頂上を書く
		drawWalls(0, rs[0], yOffset, yOffset, 0, 20);
		//	底を書く
		drawWalls(rs[rs.length - 1], 0, yOffset + (height / rs.length), yOffset + (height / rs.length), 370, 390);

		glEnd();
		glDisable(GL_TEXTURE_2D);
	}

	private void drawWalls(int r, int r2, float topY, float bottomY, int texY, int texY2) {
		int		nAngle = 15;
		int		texWidth = texture.getWidth() / (360 / nAngle);
		int		texX = texture.getWidth() - texWidth;
		int		texX2 = texture.getWidth();
		
		//	x 軸方向に nAngle 度ずつ面を描いていく
		for (int angle = 0; angle < 360; angle += nAngle) {
			int		angle2 = (angle + nAngle) % 360;
			
			double	cos = Math.cos(Math.toRadians(angle));
			double	cos2 = Math.cos(Math.toRadians(angle2));
			double	sin = Math.sin(Math.toRadians(angle));
			double	sin2 = Math.sin(Math.toRadians(angle2));
			float	topX = (float)(cos * r);
			float	topX2 = (float)(cos2 * r);
			float	topZ = (float)(sin * r);
			float	topZ2 = (float)(sin2 * r);
			float	bottomX = (float)(cos * r2);
			float	bottomX2 = (float)(cos2 * r2);
			float	bottomZ = (float)(sin * r2);
			float	bottomZ2 = (float)(sin2 * r2);

			Vector3f	normal;
			
			//	四角形の左上の三角形を描く

			//	法線を計算して設定する
			normal = getTrigangleNormal(topX, topY, topZ, topX2, topY, topZ2, bottomX2, bottomY, bottomZ2);
			glNormal3f(normal.x, normal.y, normal.z);

			//	テクスチャーを貼りつつ頂点を設定する
			texture.point(texX2, texY);
			glVertex3f(topX, topY, topZ);

			texture.point(texX, texY);
			glVertex3f(topX2, topY, topZ2);

			texture.point(texX, texY2);
			glVertex3f(bottomX2, bottomY, bottomZ2);

			//	四角形の右下の三角形を描く

			//	法線を計算して設定する
			normal = getTrigangleNormal(bottomX2, bottomY, bottomZ2, bottomX, bottomY, bottomZ, topX, topY, topZ);
			glNormal3f(normal.x, normal.y, normal.z);

			//	テクスチャーを貼りつつ頂点を設定する
			texture.point(texX, texY2);
			glVertex3f(bottomX2, bottomY, bottomZ2);

			texture.point(texX2, texY2);
			glVertex3f(bottomX, bottomY, bottomZ);

			texture.point(texX2, texY);
			glVertex3f(topX, topY, topZ);

			texX -= texWidth;
			texX2 -= texWidth;
		}
	}
	
	/**
	 *	三角形の頂点を左回りで p1(x1, y1, z1), p2(x2, y2, z2), p3(x3, y3, z3) として渡し、その法線ベクトルを返す
	 */
	private Vector3f getTrigangleNormal(float x1, float y1, float z1, float x2, float y2, float z2, float x3, float y3, float z3) {
		Vector3f	p1 = new Vector3f(x1, y1, z1);
		Vector3f	p2 = new Vector3f(x2, y2, z2);
		Vector3f	p3 = new Vector3f(x3, y3, z3);
		Vector3f	v1 = Vector3f.sub(p2, p1, null);
		Vector3f	v2 = Vector3f.sub(p3, p1, null);
		Vector3f	cross = Vector3f.cross(v1, v2, null);

		return cross.normalise(null);
	}

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

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

		if (1000 <= timeDelta) {
			//	timeDelta が 1 秒を上回ったら、FPS をリセットする
			timeDelta -= 1000;
			calcedFps = fps;
			fps = 1;
		} else {
			fps++;
		}
	}
	
	public static long getMillisecond() {
		//	現在の時間をミリ秒で返す
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}

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

LightManager.java
package test.light;

import static org.lwjgl.opengl.GL11.GL_AMBIENT;
import static org.lwjgl.opengl.GL11.GL_DIFFUSE;
import static org.lwjgl.opengl.GL11.GL_FRONT;
import static org.lwjgl.opengl.GL11.GL_LIGHT0;
import static org.lwjgl.opengl.GL11.GL_LIGHTING;
import static org.lwjgl.opengl.GL11.GL_NORMALIZE;
import static org.lwjgl.opengl.GL11.GL_POSITION;
import static org.lwjgl.opengl.GL11.GL_SPECULAR;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glLight;
import static org.lwjgl.opengl.GL11.glMaterial;

import java.nio.FloatBuffer;
import java.util.Arrays;

import org.lwjgl.BufferUtils;
import org.lwjgl.input.Keyboard;

public class LightManager {
	enum Function {
		Ambient, Diffuse, Specular
	}

	private float		xAngle = 0;
	private float		yAngle = 0;
	private Function	function = Function.Ambient;
	private float		ambient = 0.1f;
	private float		diffuse = 0.8f;
	private float		specular = 0.2f;
	private float		position = 1;
	private float		positionX = 200f;
	private float		positionY = 500f;
	private float		positionZ = 400f;

	public void initLight() {
		//	ライティングを有効にする
		glEnable(GL_LIGHTING);
		
		//	光源0 を有効にする
		glEnable(GL_LIGHT0);

		//	法線の正規化を強制する
		glEnable(GL_NORMALIZE);
	}
	
	public void update() {
		//	キーが押されている間処理を行う
		if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) {
			if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
				//	左 Crtl + ← で X 軸方向に光源を移動する
				positionX -= 3;
			} else {
				//	← で X 軸方向にモデルを回転する
				xAngle = (xAngle - 1) % 360;
			}
		} else if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) {
			if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
				//	左 Crtl + → で X 軸方向に光源を移動する
				positionX += 3;
			} else {
				//	→ で X 軸方向にモデルを回転する
				xAngle = (xAngle + 1) % 360;
			}
		} else if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
			if ((Keyboard.isKeyDown(Keyboard.KEY_LSHIFT))) {
				//	左 Shift + ↓ で Z 軸方向に光源を移動する
				positionZ += 3;
			} else if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
				//	左 Crtl + ↓ で Y 軸方向に光源を移動する
				positionY -= 3;
			} else {
				//	↓ で Y 軸方向にモデルを回転する
				yAngle = (yAngle + 1) % 360;
			}
		} else if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
			if ((Keyboard.isKeyDown(Keyboard.KEY_LSHIFT))) {
				//	左 Shift + ↑ で Z 軸方向に光源を移動する
				positionZ -= 3;
			} else if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
				//	左 Crtl + ↑ で Y 軸方向に光源を移動する
				positionY += 3;
			} else {
				//	↑ で Y 軸方向にモデルを回転する
				yAngle = (yAngle - 1) % 360;
			}
		} else if (Keyboard.isKeyDown(Keyboard.KEY_F2)) {
			//	F2 で光の設定値を減らす
			if (function == Function.Ambient) {
				ambient -= 0.01;
			} else if (function == Function.Diffuse) {
				diffuse -= 0.01;
			} else if (function == Function.Specular) {
				specular -= 0.01;
			}
		} else if (Keyboard.isKeyDown(Keyboard.KEY_F3)) {
			//	F3 で光の設定値を増やす
			if (function == Function.Ambient) {
				ambient += 0.01;
			} else if (function == Function.Diffuse) {
				diffuse += 0.01;
			} else if (function == Function.Specular) {
				specular += 0.01;
			}
		}

		//	キーが離された時点で処理を行う
		while (Keyboard.next()) {
			if ((Keyboard.getEventKey() == Keyboard.KEY_F1) && (Keyboard.getEventKeyState())) {
				//	F1 で光の種類を変更する
				int	n = Arrays.asList(Function.values()).indexOf(function);
				n = (n + 1) % Function.values().length;
				function = Function.values()[n];
			} else if ((Keyboard.getEventKey() == Keyboard.KEY_F4) && (Keyboard.getEventKeyState())) {
				//	F4 で光源の性質を変更する
				position = (position == 0)? 1: 0;
			}
		}
	}
	
	public void light() {
		//	光源を設定する

		//	位置を指定する
		glLight(GL_LIGHT0, GL_POSITION, toFloarBuffer(new float[] {positionX, positionY, positionZ, position}));

		//	環境光・モデル全体が均一に明るくなっていく
		glLight(GL_LIGHT0, GL_AMBIENT, toFloarBuffer(new float[] {ambient, ambient, ambient, ambient}));
		
		//	拡散光・光の当たっている部分が中心から明るくなっていく
		glLight(GL_LIGHT0, GL_DIFFUSE, toFloarBuffer(new float[] {diffuse, diffuse, diffuse, diffuse}));
		
		//	鏡面光・光のあたっている部分が均一に明るくなっていく
		glLight(GL_LIGHT0, GL_SPECULAR, toFloarBuffer(new float[] {specular, specular, specular, specular}));

		//	材質として、モデルの反射する光を設定する
//		glMaterial(GL_FRONT, GL_AMBIENT, toFloarBuffer(new float[] {0.1f, 0.1f, 0.1f, 1f}));
//		glMaterial(GL_FRONT, GL_DIFFUSE, toFloarBuffer(new float[] {1f, 0f, 0f, 1f}));
		//	鏡面光のみ、デフォルトでは色が設定されていないので、反映されるように色を設定する
		glMaterial(GL_FRONT, GL_SPECULAR, toFloarBuffer(new float[] {0.3f, 0.3f, 0.3f, 1f}));
	}

	/**
	 *	長さ 4 の float 配列を FloatBuffer に変換して返す
	 */
	public FloatBuffer toFloarBuffer(float[] values) {
		FloatBuffer		floats = BufferUtils.createFloatBuffer(4).put(values);

		floats.flip();
		
		return floats;
	}

	public String getLabel() {
		return String.format("%s, ambient: %.3f, diffuse: %.3f, specular: %.3f, position: %.0f (%.0f, %.0f, %.0f)",
				function.name(), ambient, diffuse, specular, position, positionX, positionY, positionZ);
	}

	public float getXAngle() {
		return xAngle;
	}

	public float getYAngle() {
		return yAngle;
	}

}

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

kunlun.png
kunlun.png

プログラムの使い方

F1   設定変更する光の種類を Ambient(環境光)→ Diffuse(拡散光)→ Specular(鏡面光)→ と切り替える
F2   設定変更中の光の値を減らす
F3   設定変更中の光の値を増やす
F4   光源を指向性光源 <=> 位置的光源で切り替える

←、→   モデルを X 軸方向に回転させる
↑、↓   モデルを Y 軸方向に回転させる

左 Crtl + ←、左 Crtl + →    光源の位置を X 軸方向に移動させる
左 Crtl + ↑、左 Crtl + ↓    光源の位置を Y 軸方向に移動させる
左 Shift + ↑、左 Shift + ↓   光源の位置を Z 軸方向に移動させる

光源を有効化する

それでは光源の設定方法について見ていきましょう。まずは光源の有効化です。
ライティングそのものの有効化と、ライディングで使う個別の光源の有効化を行います。
//	ライティングを有効にする
GL11.glEnable(GL_LIGHTING);
		
//	光源0 を有効にする
GL11.glEnable(GL_LIGHT0);
ここでは glEnable() に GL_LIGHT0 を渡し、光源0を有効化しています。LWJGL では光源は GL_LIGHT0 ~ GL_LIGHT7 まで 8 つ用意されていて、この中からいくつでも同時に使うことができます。ただし、ハードウェア的に利用できる光源の数はビデオカードによって異なる場合があるらしいです。

光源の位置を設定する

光源には位置を設定することができます。 LWJGL では、光源に関する設定には glLight() メソッドを使います。
glLight(GL_LIGHT0, GL_POSITION, toFloarBuffer(new float[] {positionX, positionY, positionZ, position}));
第一引数には、位置を設定する光源を指定します。光源0の設定なので、ここでは GL_LIGHT0 です。
第二引数には、位置を設定することを示す GL_POSITION を渡します。
第三引数には、光源の位置の座標を表す FloadBuffer オブジェクトを渡します。これは (x, y, z, w) の 4 つの要素からなる float 配列を変換したものです(サンプルプログラムでは、test.light.LightManager.toFloarBuffer() メソッドによって変換が行われています)。
光源の座標は、(x / w, y / w, z / w) として計算されます。w には 0 か 1 を指定することが多いようです。

ここで w が 0 でない時、光源は位置的光源となり、設定された座標を中心に全方位に発光します。光源に対してモデルがどこにあるかによって、モデルへの光の当たり方は変わってきます。

w が 0 の時、光源は指向性光源となり、設定された座標から原点への方向に対して光が当てられます。この場合、光源の座標は位置を表すものではなく、光の向きを表すだけになります。無限大遠方からの太陽光のように(太陽の裏側には回りこめないという前提で)、モデルがどこにあろうと光の当たる方向は一定になります。
glposition.png
GL_POSITION での光源の座標が同じであっても、光源が指向性か位置的かによってモデルにへの光の当たり方は変わってくるので、注意してください。

光源の光の種類と色の設定

光源から発せられる光には 3 種類あり、そのそれぞれについて個別に色を設定することができます。ライティングは光源からの光と光の当たるモデルの色の合成によって決まるものなので、OpenGL では光の強さは光の色によって表されます。

環境光

モデル全体に対して均一に照らされる光です。モデルの面が光源側にあろうと、そうでなかろうと、均等に光が当たっていきます。
ambient.png
写真はそれぞれ環境光を 0 と 3 に設定したものです。
これは自然界でもかなり弱い光なので、あまり大きくしないほうが自然に見えます。

設定には、glLight() の第二引数に GL_AMBIENT を、第三引数に光の色を表す RGBA 値を FloatBuffer インスタンスとして指定します。
//	環境光・モデル全体が均一に明るくなっていく
glLight(GL_LIGHT0, GL_AMBIENT, toFloarBuffer(new float[] {ambient, ambient, ambient, ambient}));

拡散光

モデルの光源に近い部分ほど明るく照らされる光です。拡散光で照らされることで、光源と反対側にある面は陰になって暗いままとなります。
また、光に照らされ方も、光のあたっている部分が中心から徐々に明るくなっていくという性質があります。
diffuse.png
写真はそれぞれ拡散光を 0 と 1.3 に設定したものです。

設定には、glLight() の第二引数に GL_DIFFUSE を、第三引数に光の色を表す RGBA 値を FloatBuffer インスタンスとして指定します。
//	拡散光・光の当たっている部分が中心から明るくなっていく
glLight(GL_LIGHT0, GL_DIFFUSE, toFloarBuffer(new float[] {diffuse, diffuse, diffuse, diffuse}));

鏡面光

金属光沢などで見られるハイライト用の光です。拡散光と同じく光源と反対側にある面は陰になって暗いままとなりますが、光源側は照らされ方にあまり差がなく、ほぼ均一に明るくなっていきます。
specular.png
写真はそれぞれ鏡面光を 0 と 2 に設定したものです。
拡散光と同時に設定してしまうと光の当たる部分が被るため、綺麗なハイライトにするにはなかなか設定が難しいようです。
設定には、glLight() の第二引数に GL_SPECULAR を、第三引数に光の色を表す RGBA 値を FloatBuffer インスタンスとして指定します。
//	鏡面光・光のあたっている部分が均一に明るくなっていく
glLight(GL_LIGHT0, GL_SPECULAR, toFloarBuffer(new float[] {specular, specular, specular, specular}));

材質の光の種類と色の設定

モデル側でも、モデルの材質として、どのような色を反射するかを設定することができます。モデルの頂点座標を指定する際に個別指定できるため、モデル毎に異なる色を反射させることができます。
光源の光には 3 種類ありましたが、モデルの材質の光にもいくつか種類があります。
//	材質の環境光の色を設定する
//	glMaterial(GL_FRONT, GL_AMBIENT, toFloarBuffer(new float[] {0.1f, 0.1f, 0.1f, 1f}));

//	材質の拡散光の色を設定する
//	glMaterial(GL_FRONT, GL_DIFFUSE, toFloarBuffer(new float[] {1f, 0f, 0f, 1f}));

//	材質の鏡面光の色を設定する
//	鏡面光のみ、デフォルトでは色が設定されていないので、反映されるように色を設定する
glMaterial(GL_FRONT, GL_SPECULAR, toFloarBuffer(new float[] {0.3f, 0.3f, 0.3f, 1f}));
材質の光の設定には glMaterial() メソッドを使います。
第一引数には、光を設定する面の種類を指定します。GL_FRONT ならば表側、GL_BACK ならば裏側、GL_FRONT_AND_BACK ならば両面となります。
第二引数には、光の種類を渡します。光源の光の種類にあわせて、環境光や拡散光などを指定します。
第三引数には、光の色を表す RGBA 値を FloatBuffer インスタンスで指定します。これにより、第二引数で指定された種類の光によって何色を反射するかを設定することができます。
デフォルトでは鏡面光のみ色が設定されていないため、サンプルプログラムでは鏡面光だけ glMaterial() での設定を行っています。

ライティングを有効にした場合、モデルの頂点座標を指定する際に使った頂点への色付け(glColor4f())は無視されます。モデルに色を付けたい場合は glMaterial() を利用しましょう。

他のモデルによる影はつかない

OpenGL のライティングでは、他のモデルによってできる影は描画されません。あくまで自分自身に対する光の当たり方を計算するものなので、例えば光源とモデルの間に他のモデルがあっても、光が遮られることはありません。
shadows.png
他のモデルの作る影の計算は別途自分で行う必要があります。モデルの位置や数によって処理が変わるため難しく、なかなか正解が見つかっていないテーマのようです。

法線

ライティングにおける色の設定は glLight()、glMaterial() メソッドで行いましたが、この他に光の反射する方向を設定する必要があります。光源に対して表側にある面と裏側にある面とでは光を反射する方向が異なり、見え方は変わってきます。この光の反射する方向を計算するのに使われるのが、法線です。

面に対して垂直な単位ベクトル(正規化され、大きさが 1 のベクトル)を面法線と呼びます。
他にも頂点に対する法線があり、こちらは頂点法線と呼ばれています(サンプルプログラムでは面法線を扱っています)。
法線は面の向きを表すもので、光の入射角と反射角を計算する基準として使われています。これは自然界での光の反射そのままです。法線が正しい方向に設定されていないと、光源の位置に対する影の付き方がおかしくなってしまいます。

法線の設定

法線の設定は、glNormal3f() メソッドで行うことができます。引数は先頭から、法線ベクトルの x 要素、y 要素、z 要素です。
//	表示する三角形の法線ベクトルを計算する
Vector3f	normal = getTrigangleNormal(topX, topY, topZ, topX2, topY, topZ2, bottomX2, bottomY, bottomZ2);

//	法線を設定する
glNormal3f(normal.x, normal.y, normal.z);

//	三角形の頂点座標を指定する
glVertex3f(topX, topY, topZ);
glVertex3f(topX2, topY, topZ2);
glVertex3f(bottomX2, bottomY, bottomZ2);
glVertex3f() の前で glNormal3f() を呼び出すことで、その頂点に対して法線が設定されます。ここでは面法線を設定しているので、glNormal3f() の後で 3 つの頂点すべてを glVertex3f() で指定しています。
頂点法線を利用する場合は、各頂点の指定の前に毎回 glNormal3f() を呼び出すことになります。

デフォルトでは法線ベクトルには (0, 0, 1) が設定されていて、z 軸方向に手前に向かって設定されています。自分で法線を設定せずにモデルを表示すると、すべての面が手前方向に(向いている法線に基づいて)光を反射するようになります。

面法線の求め方

さて肝心の面法線の求め方について触れてみたいと思います。ここからは高校数学とかが絡んできます。(一応動作チェックまでしていますが、もしご指摘などありましたらぜひぜひよろしくお願いします)

面法線は、面の 3 つの頂点から求められます。
頂点が 4 つある面は、4 つめの頂点が同一平面上にあるという保証はなくなるので、3 点毎に(三角形に分割して)面法線を求めます。四角形が三角形 2 つで構成されるように、四角形以上の多角形も、1 つの面に対して必要な回数三角形を描いていくことで扱うことができます。
normal.png
三角形の頂点を左回りで p1(x1, y1, z1), p2(x2, y2, z2), p3(x3, y3, z3) とします。
計算にはベクトルを使うので、まずは 3 点を lwjgl_util.jar に含まれる Vector3f クラスのインスタンスに変換します。
Vector3f	p1 = new Vector3f(x1, y1, z1);
Vector3f	p2 = new Vector3f(x2, y2, z2);
Vector3f	p3 = new Vector3f(x3, y3, z3);
p1、p2、p3 は点ですが、原点から各点までのベクトルとして扱えますので、Vector3f インスタンスとしても表すことができます。
(ベクトルなので、各要素は 終点座標 - 開始座標 となるはずですが、開始地点が原点なので - 0 することになるため、そのまま x1, y1, z1 を持たせれば OK です)

次に 1 つめの頂点を開始座標とする、三角形の 2 辺をベクトルとして求めます。p1 → p2 のベクトルを v1 、p1 → p3 のベクトルを v2 とします。
Vector3f	v1 = Vector3f.sub(p2, p1, null);
Vector3f	v2 = Vector3f.sub(p3, p1, null);
v1, v2 はそれぞれ終点のベクトルから開始地点のベクトルを引くことで求められます。
org.lwjgl.util.vector.Vector3f.sub() がベクトルの差を計算するメソッドです。第一引数には終点のベクトル、第二引数には開始地点のベクトル、第三引数には null を渡します。

計算した v1, v2 の外積を求めます。
Vector3f	cross = Vector3f.cross(v1, v2, null);
org.lwjgl.util.vector.Vector3f.cross() がベクトルの外積を計算するメソッドです。第一引数には左側のベクトル、第二引数には右側のベクトル、第三引数には null を渡します。左側というのは、ベクトルを構成する頂点座標の順序(p1, p2, p3)が先にあるものです。

この外積が法線ベクトルとなるのですが、向きはともかくとして大きさが頂点座標によってまちまちになっているので、最後に正規化して大きさを 1 に揃えてやります。
cross.normalise(null);
org.lwjgl.util.vector.Vector3f.normalise() が、ベクトルを正規化するメソッドです。引数には null を渡します。

これで三角形の面法線を求めることができます。
サンプルプログラムでは test.light.Lighting.getTrigangleNormal() が面法線を計算するメソッドになっています。

法線の正規化を強制する

法線ベクトルは正規化されている必要があります。これは、法線の大きさが反射する光の強さに影響するためで、正規化されていないとコントラストが変わってしまいます。例えば法線ベクトルが大きすぎると明るくなりすぎてしまい、見えるべき影の微妙な陰影が見えなくなってしまいます。

ところが、設定した法線は glScalef() による拡大縮小にも影響されてしまうため、法線設定時には正規化されていてもその前後で大きさが変わる場合があります。これを回避するため、設定された法線の正規化を強制する機能があります。
glEnable(GL_NORMALIZE);
法線の正規化機能は glEnable() の引数に GL_NORMALIZE を設定することで有効化できます。

頂点法線

先ほどさらっと取り上げましたが、頂点法線と呼ばれる法線があります。これは特定の頂点に対して垂直な単位ベクトルです。
頂点法線は、特定の頂点が属するすべての面の面法線を足して計算されます。法線が隣にあるすべての面の向きによって補正されていくイメージです。
面法線では面のすべての頂点が同じ法線を持つことになりますが、頂点法線では面の各頂点の法線がそれぞれ変わってきます。よって、面と面との境界における陰影がより滑らかになり、曲面や球体をライティングする際には効果を発揮します。

隣接している面をすべて求めるなど計算が大変なので、サンプルプログラムでは扱っていません。面法線の計算もそうなのですが、頂点法線の計算は普通は 3D モデリングソフトがよろしくやってくれるので自分でやる必要ありません。

正多角形でないポリゴンにテクスチャーを貼る

ライティングからは話がずれますが、最後にテクスチャーマッピングについて少し補足をしておきたいと思います。

OpenGL では、ポリゴンとポリゴンに貼るテクスチャーのサイズが揃っている必要はありません。テクスチャーはポリゴンに対して適切に拡大・縮小されて貼り付けられるので、頂点の数さえあっていれば形については気にする必要はありません。

サンプルプログラムではモデルは(台形を 2 分割しているので)直角三角形ではありませんが、貼り付けるテクスチャーは(長方形を頂点で分割していくので)直角三角形になっています。
モデルの各面の形に合わせてテクスチャーを用意しなくてもいいので、この性質はとてもありがたいです。

おわり!

今回のモデルは封神演義の崑崙山です。
テクスチャにも一応岩肌っぽい明暗を付けているのですが、ライティングの影響であんまり質感は目立ってませんね。それでも崑崙山の文字は浮き彫りっぽく見えるよう、テクスチャー自体に軽くエンボス加工を施してます。正確な陰影をつけるにはきっちり文字を浮き彫りにしてモデリングする必要がありますが、ポリゴン数は増えるし大変なので、テクスチャーに直接書き込んでしまっています。これは市販のゲームでもよく見られる手法ですね。
もちろん、光はテクスチャーに書き込まれているので、光源の位置が変わろうと陰やハイライトの方向は変わりません。

モデルには、法線をもじってナデシコの砲戦フレームでも使おうかとも考えたのですが、あれをモデリングするスキルはさすがに私にはないので断念しました。崑崙山の座標はコード中でハードコーディングしていますが、これは直接数値をいじりながら調整したものです。普通モデリングには 3D モデリングソフトを使いますので、こんなに頑張る(一応頑張ったんですよ…こんなですが… TT)のは今回が最後だと思います。
ブログ記事用のサンプルだし、まあこんなもんでいいかなーという感じですけどね。

…いっしょにゲームを作ってくれるモデル職人さんを募集中です!

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


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