RSS2.0

LWJGL で画像を表示してみる

今日は画像の表示をしてみたいと思います。ゲーム作りでもかなりテンションのあがる箇所ですねー。やったー!

OpenGL では、画像はテクスチャーとして読み込み、ポリゴンにぺたっと貼り付けることで表示します。テクスチャーをポリゴンに割り当てることを、テクスチャーマッピングといいます。
例の立方体に png 画像を貼りつけて、こんな感じにしてみました。
73ef6cd4006590487b09b000b13f97ac7cd5.png
6面それぞれに別々の画像を貼り付けています。なんだか怪しい出来に(笑

ソースコード

いつも通り、まずはソースを実行してみてくださいー。
前回までのプロジェクトに test.monolith.v3 というパッケージを作って、その中に以下の5つのクラスを置いてください。
また、立方体の回転に作った以下のファイルも必要になりますので、それぞれ昔のパッケージのままプロジェクト内に残しておいてください。
・test.monolith.v1.Direction
・test.monolith.v1.Point

TextureMonolith.java
package test.monolith.v3;

import static org.lwjgl.opengl.GL11.GL_BACK;
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.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.glRotatef;
import static org.lwjgl.opengl.GL11.glScalef;
import static org.lwjgl.opengl.GL11.glTranslatef;

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

import test.monolith.v1.Direction;

public class TextureMonolith {
	private int			width = 450;
	private int			height = 320;
	private int			depth = 300;

	private float		   xAngle = 0;
	private float		   yAngle = 0;
	private Direction	   input = null;
	
	private Texture			texture;

	public void start() {
		try {
			//	ウインドウを生成する
			Display.setDisplayMode(new DisplayMode(width, height));
			Display.setTitle("Textured Monolith");
			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 {
		//	ファイルのパス指定で画像を読み込む
		texture = new TextureLoader().loadTexture("images/texture.png");

		//	クラスパスから画像を検索して読み込む
//		texture = new TextureLoader().loadTexture("images/texture.png", this.getClass().getClassLoader());
	}
	
	private void terminate() {
		if (texture != null) texture.dispose();
	}

	private void update() {
		input = Direction.getPressing();
		
		if (input != null) {
			xAngle = input.rotateXAngle(xAngle);
			yAngle = input.rotateYAngle(yAngle);
		}
	}

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

		//	画面中央の座標を (0, 0, 0) とするよう座標系を移動する
		glTranslatef(width / 2f, height / 2f, depth / -2f);

		//	座標のスケールを指定する
		//	ここで指定した x, y, z 毎の係数が、次に指定する座標にそれぞれ掛けられる
		glScalef(30f, 30f, 30f);

		glRotatef(xAngle, 0, 1, 0);	//	y 軸を中心に xAngle 度回転させる
		glRotatef(yAngle, 1, 0, 0);	//	x 軸を中心に yAngle 度回転させる
		
		//	テクスチャをバインドする
		texture.bind();
		
		glBegin(GL_QUADS);

		glColor4f(1f, 1f, 1f, 1f);
		
		for (Face face: Face.values()) {
			face.draw(texture, 1f);
		}
		
		glEnd();
	}

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

}

Face.java
package test.monolith.v3;

import static test.monolith.v1.Point.P_BACK_BOTTOM_LEFT;
import static test.monolith.v1.Point.P_BACK_BOTTOM_RIGHT;
import static test.monolith.v1.Point.P_BACK_TOP_LEFT;
import static test.monolith.v1.Point.P_BACK_TOP_RIGHT;
import static test.monolith.v1.Point.P_FRONT_BOTTOM_LEFT;
import static test.monolith.v1.Point.P_FRONT_BOTTOM_RIGHT;
import static test.monolith.v1.Point.P_FRONT_TOP_LEFT;
import static test.monolith.v1.Point.P_FRONT_TOP_RIGHT;
import static test.monolith.v3.Bounds.B_BACK;
import static test.monolith.v3.Bounds.B_BOTTOM;
import static test.monolith.v3.Bounds.B_FRONT;
import static test.monolith.v3.Bounds.B_LEFT;
import static test.monolith.v3.Bounds.B_RIGHT;
import static test.monolith.v3.Bounds.B_TOP;
import test.monolith.v1.Point;

public enum Face {
	F_FRONT(new Point[] {
			P_FRONT_TOP_RIGHT,
			P_FRONT_TOP_LEFT,
			P_FRONT_BOTTOM_LEFT,
			P_FRONT_BOTTOM_RIGHT
	}, B_FRONT),
	F_BACK(new Point[] {
			P_BACK_TOP_LEFT,
			P_BACK_TOP_RIGHT,
			P_BACK_BOTTOM_RIGHT,
			P_BACK_BOTTOM_LEFT,
	}, B_BACK),
	F_LEFT(new Point[] {
			P_FRONT_TOP_LEFT,
			P_BACK_TOP_LEFT,
			P_BACK_BOTTOM_LEFT,
			P_FRONT_BOTTOM_LEFT
	}, B_LEFT),
	F_RIGHT(new Point[] {
			P_BACK_TOP_RIGHT,
			P_FRONT_TOP_RIGHT,
			P_FRONT_BOTTOM_RIGHT,
			P_BACK_BOTTOM_RIGHT
	}, B_RIGHT),
	F_TOP(new Point[] {
			P_BACK_TOP_RIGHT,
			P_BACK_TOP_LEFT,
			P_FRONT_TOP_LEFT,
			P_FRONT_TOP_RIGHT			
	}, B_TOP),
	F_BOTTOM(new Point[] {
			P_FRONT_BOTTOM_RIGHT,
			P_FRONT_BOTTOM_LEFT,
			P_BACK_BOTTOM_LEFT,
			P_BACK_BOTTOM_RIGHT
	}, B_BOTTOM);

	private final Point[]  points;
	private final Bounds   bounds;
	
	private Face(Point[] points, Bounds bounds) {
		this.points = points;
		this.bounds = bounds;
	}

	public void draw(Texture texture, float alpha) {
		bounds.pointTopRight(texture);
		points[0].point();
		bounds.pointTopLeft(texture);
		points[1].point();

		bounds.pointBottomLeft(texture);
		points[2].point();
		bounds.pointBottomRight(texture);
		points[3].point();
	}

}

Bounds.java
package test.monolith.v3;

public enum Bounds {
	B_FRONT(80, 80, 240, 400),
	B_BACK(320, 80, 480, 400),
	B_LEFT(0, 80, 80, 400),
	B_RIGHT(240, 80, 320, 400),
	B_TOP(80, 0, 240, 80),
	B_BOTTOM(80, 400, 240, 480)
	;
	
	private final int		x1;
	private final int		y1;
	private final int		x2;
	private final int		y2;
	
	private Bounds(int x1, int y1, int x2, int y2) {
		this.x1 = x1;
		this.y1 = y1;
		this.x2 = x2;
		this.y2 = y2;
	}

	public void pointTopLeft(Texture texture) {
		texture.point(x1, y1);
	}

	public void pointTopRight(Texture texture) {
		texture.point(x2, y1);
	}

	public void pointBottomLeft(Texture texture) {
		texture.point(x1, y2);
	}

	public void pointBottomRight(Texture texture) {
		texture.point(x2, y2);
	}
}

TextureLoader.java
/**
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 **/
package test.monolith.v3;

import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_RGB;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glGenTextures;
import static org.lwjgl.opengl.GL11.glTexImage2D;
import static org.lwjgl.opengl.GL11.glTexParameteri;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.Hashtable;

import javax.imageio.ImageIO;

import org.lwjgl.BufferUtils;

public class TextureLoader {
	private final ColorModel		glAlphaColorModel = new ComponentColorModel(
			ColorSpace.getInstance(ColorSpace.CS_sRGB),
			new int[] {8,8,8,8},
			true,
			false,
			ComponentColorModel.TRANSLUCENT,
			DataBuffer.TYPE_BYTE);
	private final ColorModel		glColorModel = new ComponentColorModel(
			ColorSpace.getInstance(ColorSpace.CS_sRGB),
			new int[] {8,8,8,0},
			false,
			false,
			ComponentColorModel.OPAQUE,
			DataBuffer.TYPE_BYTE);
	private final IntBuffer			textureIDBuffer = BufferUtils.createIntBuffer(1);

	/**
	 *	テクスチャーIDを生成する
	 */
	private int createTextureID() {
		glGenTextures(textureIDBuffer);

		return textureIDBuffer.get(0);
	}

	/**
	 *	指定されたパスの画像ファイルをテクスチャーに変換して返す
	 */
	public Texture loadTexture(String imagePath) throws IOException {
		return loadTexture(ImageIO.read(new FileInputStream(imagePath)));
	}

	/**
	 *	指定されたパスの画像ファイルを、指定された ClassLoader で探して、テクスチャーに変換して返す
	 */
	public Texture loadTexture(String imagePath, ClassLoader classLoader) throws IOException {
		return loadTexture(ImageIO.read(classLoader.getResourceAsStream(imagePath)));
	}

	/**
	 *	BufferedImage をテクスチャーに変換して返す
	 */
	public Texture loadTexture(BufferedImage image) throws IOException {
		return loadTexture(image, GL_TEXTURE_2D, GL_RGBA, GL_LINEAR, GL_LINEAR);
	}

	private Texture loadTexture(BufferedImage image, int target, int dstPixelFormat, int minFilter, int magFilter) throws IOException {
		//	テクスチャー ID を生成する
		int		textureID = createTextureID();
		Texture	texture = new Texture(target, textureID);
		
		texture.setWidth(image.getWidth());
		texture.setHeight(image.getHeight());

		//	glTexImage2D() の対象となるテクスチャー ID をバインドする
		glBindTexture(target, textureID);

		//	アルファ値の有無を確認する
		int		srcPixelFormat;

		if (image.getColorModel().hasAlpha()) {
			srcPixelFormat = GL_RGBA;
		} else {
			srcPixelFormat = GL_RGB;
		}

		//	BufferedImage をテクスチャー用のバイト配列に変換する 
		ByteBuffer	textureBuffer = convertImageData(image, texture);

		//	画像の拡大・縮小時の補間方法を設定する
		if (target == GL_TEXTURE_2D) {
			glTexParameteri(target, GL_TEXTURE_MIN_FILTER, minFilter);
			glTexParameteri(target, GL_TEXTURE_MAG_FILTER, magFilter);
		}

		//	バイト配列と色情報のフォーマットからテクスチャーを生成する
		glTexImage2D(target,
					  0,
					  dstPixelFormat,
					  get2Fold(image.getWidth()),
					  get2Fold(image.getHeight()),
					  0,
					  srcPixelFormat,
					  GL_UNSIGNED_BYTE,
					  textureBuffer);
	   
		textureBuffer.clear();

		return texture;
	}

	/**
	 *	指定さえれた値より大きい 2 べき乗を求める
	 */
	private static int get2Fold(int fold) {
		int ret = 2;

		while (ret < fold) {
			ret *= 2;
		}
		return ret;
	}

	/**
	 *	テクスチャーの元データとなるバイト配列をつくり、BufferedImage を描画して返す
	 */
	private ByteBuffer convertImageData(BufferedImage bufferedImage, Texture texture) {
		ByteBuffer		imageBuffer;
		WritableRaster	raster;
		BufferedImage	texImage;

		int				texWidth = texture.getTextureWidth();
		int				texHeight = texture.getTextureHeight();

		if ((texWidth <= 0) || (texHeight <= 0)) {
			texWidth = 2;
			texHeight = 2;

			//	テクスチャーの大きさは 2 のべき乗でなければならないため、画像本来の大きさ以上で縦横の長さを計算する
			while (texWidth < bufferedImage.getWidth()) {
				texWidth *= 2;
			}
			while (texHeight < bufferedImage.getHeight()) {
				texHeight *= 2;
			}
			
			texture.setTextureHeight(texHeight);
			texture.setTextureWidth(texWidth);
		}

		//	テクスチャーの元となるデータを作成する
		//	変換する画像がアルファ値を含むかどうかを、テクスチャーのデータ形式に反映させる
		if (bufferedImage.getColorModel().hasAlpha()) {
			raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, texWidth, texHeight, 4, null);
			texImage = new BufferedImage(glAlphaColorModel, raster, false, new Hashtable());
		} else {
			raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, texWidth, texHeight, 3, null);
			texImage = new BufferedImage(glColorModel, raster, false, new Hashtable());
		}

		//	変換する画像のアルファモードを調べる
		texture.setAlphaPremultiplied((bufferedImage.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE));

		//	テクスチャーの元データに、変換する画像を描画する
		Graphics	g = texImage.getGraphics();

		g.setColor(new Color(0f, 0f, 0f, 0f));
		g.fillRect(0, 0, texWidth, texHeight);	//	画像は最初に透明色で塗りつぶす
		g.drawImage(bufferedImage, 0, 0, null);
		g.dispose();
		
		//	読み込んだ画像を破棄する
		bufferedImage.flush();

		//	テクスチャーの元データをバイト配列に変換する
		byte[] data = ((DataBufferByte) texImage.getRaster().getDataBuffer()).getData();
		texImage.flush();

		imageBuffer = ByteBuffer.allocateDirect(data.length);
		imageBuffer.order(ByteOrder.nativeOrder());
		imageBuffer.put(data, 0, data.length);
		imageBuffer.flip();

		return imageBuffer;
	}

	/**
	 *	テクスチャーに使われるデータ形式と同じデータ形式で、指定のサイズの BufferedImage を生成して返す
	 */
	public BufferedImage createImageData(int width, int height) {
		WritableRaster	raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 4, null);
		BufferedImage	bufferedImage = new BufferedImage(glAlphaColorModel, raster, true, new Hashtable());

		return bufferedImage;
	}
}

Texture.java
/*
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package test.monolith.v3;

import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glDeleteTextures;
import static org.lwjgl.opengl.GL11.glTexCoord2f;

public class Texture {
	private int		target;
	private int		textureID;

	private int		width;
	private int		height;

	private int		texWidth;
	private int		texHeight;
	
	private boolean	isAlphaPremultiplied;

	public Texture(int target, int textureID) {
		this.target = target;
		this.textureID = textureID;
		this.isAlphaPremultiplied = true;
	}
	
	public void point(int srcX, int srcY) {
		float	tx = 1.0f * srcX / texWidth;
		float	ty = 1.0f * srcY / texHeight;

		glTexCoord2f(tx, ty);
	}

	void setTextureHeight(int texHeight) {
		this.texHeight = texHeight;
	}

	void setTextureWidth(int texWidth) {
		this.texWidth = texWidth;
	}
	
	int getTextureWidth() {
		return texWidth;
	}

	int getTextureHeight() {
		return texHeight;
	}

	public int getWidth() {
		return width;
	}

	void setWidth(int width) {
		this.width = width;
	}

	public int getHeight() {
		return height;
	}

	void setHeight(int height) {
		this.height = height;
	}

	public boolean isAlphaPremultiplied() {
		return isAlphaPremultiplied;
	}

	void setAlphaPremultiplied(boolean isAlphaPremultiplied) {
		this.isAlphaPremultiplied = isAlphaPremultiplied;
	}

	public void dispose() {
		if (0 < textureID) {
			glDeleteTextures(textureID);
			textureID = -1;
		}
	}

	public void bind() {
		glBindTexture(target, textureID);
	}
	
}

画像ファイル

ポリゴンに貼りつける画像です。
mvn プロジェクトのディレクトリ内に images というディレクトリを作って、その中に下の画像ファイルを texture.png という名前で保存してください。

texture.png
ffdc334a069440458b0abc702009019ba120.png
mvn プロジェクトが C:\eclipse\workspace\test ならば、表示する画像ファイルのパスは C:\eclipse\workspace\test\images\texture.png となります。

ひとまず説明の簡略化のために、画像はファイルパス指定で読み込みたいと思います。Java らしくクラスパスから検索する方法については最後に記載しますね。

テクスチャーの生成

 画像ファイルを読み込んでテクスチャー用バイト配列に変換し、テクスチャーを生成します。残念ながらこの処理は LWJGL では提供されていないため、自前で実装しなければなりません。OpenGL の API でもバイト配列を受け取るだけなので、LWJGL としてもそれ以上のことはしないというポリシーなんでしょうかね。

 この処理は結構面倒くさいのですが、公式サイトのデモページテクスチャー読み込みのサンプルが公開されているので、今回はこれに少し手を加えて使っていきたいと思います。

 ちなみに、slick という LWJGL ベースの 2D ゲーム用ライブラリでもこの辺の機能が提供されています。

テクスチャーを読み込む

TextureLoader クラスがテクスチャーの生成を行い、読み込んだ個々のテクスチャーを Texture クラスのインスタンスとして返します。
それぞれ元のソースコードは
Texture.java
TextureLoader.java
となります。こちらは LWJGL と同じく BSD ライセンスです。

テクスチャーの読み込みは、TextureLoader.loadTexture() で行っています。
//	ファイルのパス指定で画像を読み込む
texture = new TextureLoader().loadTexture("images/texture.png");
 パスには、読み込む画像ファイルのパスを指定します。相対パスで指定する場合はカレントディレクトリがプログラムの実行ディレクトリ(maven プロジェクトのディレクトリ)になることに注意してください。返される Texture インスタンスが読み込んだテクスチャーになります。
 読み込める画像は、内部的に呼び出している javax.imageio.ImageIO.read(InputStream) が認識できるファイル形式ならなんでも OK です。jpg、png、gif、bmp などのファイル形式が読み込めます。

 TextureLoader 内のデータ変換処理については深入りしなくてもいい気がするので、大まかに説明しておきます。
 Java 定番の InputStream として表示する画像を開き、それを javax.imageio.ImageIO.read(InputStream) で java.awt.image.BufferedImage として読み込みます。次にテクスチャーの元データとなる BufferedImage を、テクスチャー用の java.awt.image.Raster で別に作り、表示する画像を描き込みます。最後にテクスチャーの BufferedImage を java.nio.ByteBuffer に変換します。ByteBuffer からテクスチャーを生成するのには、LWJGL の glTexImage2D() メソッドを使います。glTexImage2D() は OpenGL の API ですね。

テクスチャーID

 OpenGL がテクスチャーを扱う際には、テクスチャー ID を呼ばれる数値で個々のテクスチャーを識別します。7 番のテクスチャーを貼るよ~とかいう感じですね。Texture クラスに textureID という int 型のフィールドがありますが、名前の通り、これがテクスチャー ID を格納しています。
 テクスチャー ID はテクスチャーを一意に識別しますが、実際にはこれはテクスチャーの読み込まれたメモリのアドレスに対応するようです(OpenGL の実装によるのかも)。Java では 1 からの連番として取り扱っていました。
 テクスチャー ID の割り当ては、TextureLoader.loadTexture() 内で呼び出されている LWJGL の glGenTextures() で行われます。
 glGenTextures() の実装内容はともかくとして、これで得られるテクスチャー ID を取り違えないように注意してください。

テクスチャーの大きさ

 テクスチャーのサイズは、縦横ともに 2 のべき乗の長さであることが望ましいです。TextureLoader では画像読み込み時に、元の画像のサイズより大きい 2 のべき乗をもとめてテクスチャーのサイズとしています。例えば 7 x 10 ピクセルの画像は、8 x 16 ピクセルのテクスチャーに変換しまています。
 初期の OepngGL では、サイズは必ず 2 のべき乗でなければなりませんでしたが、OpenGL 2.0 からはそうでないサイズのテクスチャーも使えるようになりました。ただ、2 のべき乗の長さでない場合はテクスチャーの表示に時間がかかるという検証結果もあるので、なるべくサイズをあわせたテクスチャーを作るほうが良さそうです。

 参考: non-power-of-two texture の速度を検証する
 non-power-of-two texture の速度を検証する

 テクスチャーのサイズを調整する際に作られる余白は、元の画像にはそもそも存在しない領域なので、無駄なデータとなります。メモリに読み込まれるデータを有効活用するという意味でも、意識的に元の画像のサイズを2のべき乗に合わせていくといいですね。

テクスチャーをポリゴンにマッピングする

 それでは画像を表示してみましょう。読み込んだテクスチャーをポリゴンにマッピングすることで、画像を表示できます。
 テクスチャーを貼り付ける時には、貼り付けるテクスチャーの領域を指定することができます。テクスチャーの中の一部分だけを取り出してマッピングすることもできますし、テクスチャー全体をまるごと貼り付けることもできます。
 貼り付けるポリゴンの大きさとテクスチャーの表示領域の大きさが異なる場合、テクスチャーは自動で拡大・縮小されて貼り付けられます。

テクスチャーマッピングの有効化

まずはテクスチャーのマッピングを有効化します。LWJGL ではおなじみの glEnable() に引数 GL_TEXTURE_2D を渡すことで、テクスチャーマッピングを有効化することができます。
//	テクスチャーを有効化する
glEnable(GL_TEXTURE_2D);

テクスチャーをマッピングする

 四角いポリゴンにテクスチャーを貼り付けてみます。
 サンプルプログラムでは TextureMonolith.render() 内での処理になりますが、ここではテクスチャーのマッピングの流れを把握しやすいように、簡略化したコードを書いてみたいと思います。座標(7, 7, 0)に縦横 100 の四角形のポリゴンを配置し、そこにテクスチャー内の座標(80, 80)から縦横 200 ピクセルの領域を貼り付けます。
//	表示する画像のテクスチャーをバインドする
glBindTexture(GL_TEXTURE_2D, textureID);

//	四角形を描く
glBegin(GL_QUADS);

//	色情報を設定する。アルファ値は 1f (非透過)。
glColor4f(1f, 1f, 1f, 1f);

//	右上の頂点を指定する
glTexCoord2f((280f / texureWidth), (80f / textureHeight));	//	右上の頂点に対応する、テクスチャー内の座標を指定する
glVertex3f(107, 107, 0);

//	左上の頂点を指定する
glTexCoord2f((80f / texureWidth), (80f / textureHeight));	//	左上の頂点に対応する、テクスチャー内の座標を指定する
glVertex3f(7, 107, 0);

//	左下の頂点を指定する
glTexCoord2f((80f / texureWidth), (280f / textureHeight));	//	左下の頂点に対応する、テクスチャー内の座標を指定する
glVertex3f(7, 7, 0);

//	右下の頂点を指定する
glTexCoord2f((280f / texureWidth), (280f / textureHeight));	//	右下の頂点に対応する、テクスチャー内の座標を指定する
glVertex3f(107, 7, 0);

glEnd();
テクスチャーを貼り付ける時には、まずマッピングするテクスチャーをバインドします。LWJGL の glBindTexture() を呼び出し、第二引数にバインドするテクスチャーの ID を渡します。

glColor4f() でいつも通りポリゴンの色を設定します。画像をそのまま表示するには RGB 値に白(1f, 1f, 1f)を指定しますが、下色として色をつけたい場合には好きな色を指定することもできます。半透明表示でやったように、アルファブレンドを併用して半透明で貼り付けることもできます。その場合はアルファブレンドで必要な設定を忘れないでください。

最後に、ポリゴンの各頂点にテクスチャー内のどの位置を割り当てるかを指定します。これには LWJGL の glTexCoord2f() でテクスチャー内の位置(x, y)を指定します。
この時、glTexCoord2f() の引数には 0f ~ 1f までの値を渡します。テクスチャーの横幅が 100 ピクセルの時、x = 0 の位置なら 0f、x = 50 の位置なら 0.5f、x = 100 の位置なら 1f になります。表示する画像のサイズに対する割合ではなく、テクスチャーのサイズに対する割合になるので、注意してください。
glTexCoord2f() の呼び出しは、ポリゴンの頂点座標の指定の前に行ってください。これを4つの頂点それぞれについて行えば、画像を表示することができます。

テクスチャーを破棄する

 生成したテクスチャーは不要になったタイミングで必ず破棄しましょう。
//	テクスチャーを破棄する
glDeleteTextures(textureID);
 テクスチャーの破棄には LWJGL の glDeleteTextures() メソッドを呼び出します。引数には破棄するテクスチャーの ID を渡してください。
 テクスチャーの生成にはあまり軽視できないくらいの時間がかかりますが、不要なテクスチャーをいつまでも保持しておくとメモリを圧迫してしまうので、それはそれで問題です。いかに効率的に読み込んでおくテクスチャーを切り替えるか、読み込み時間をユーザーに感じさせないようにするかがゲーム開発での重要な要素ですね。

クラスパスから画像を検索して読み込む

今回は表示する画像をパス指定で読み込んでいますが、Java のクラスパスから検索することもできます。ここは Java のクラスパスの話になります。

maven プロジェクトのファイル構成では、クラスパスから検索するリソースは src\main\resources 下に起きます。ここに置いたリソースは mvn package を走らせるとデフォルトで jar に含まれるので便利です。

resources ディレクトリは空の maven プロジェクトを作った場合にはないので、自分で作ってください。
resources 下の images ディレクトリの中に画像ファイルを置いたら、再度プロジェクトを Eclipse 用に変換します。
E:\>cd /d C:\eclipse\workspace\test
C:\eclipse\workspace\test> mvn eclipse:eclipse
あとは Eclipse 上で読み込んだプロジェクトを更新(パッケージエクスプローラーでプロジェクトを選択して F5)すれば、resources ディレクトリが実行時に認識されるようになります。
ただし、画像ファイルなどをリソースとして認識させる場合、リソースの追加や更新をしたら必ず Eclipse 上でもプロジェクトの更新を行ってください。クラスパスからリソースを検索する場合、現在認識されている Eclipse プロジェクトのリソースが検索対象となるためです。

それでは Java のコードです。
test.monolith.v3.TextureLoader.loadTexture(String, ClassLoader) を呼び出すことで、クラスパスから画像ファイルを検索できます。
//	クラスパスから画像を検索して読み込む
texture = new TextureLoader().loadTexture("images/texture.png", this.getClass().getClassLoader());
 第1引数には、resources ディレクトリからのリソースの相対パスを渡します。
 第2引数には、画像ファイルを読み込む ClassLoader として、このプロジェクトのパッケージに属するクラスから ClassLoader を取得して渡しています。ClassLoader は自分のパッケージを検索場所の基点とするので、自分の ClassLoader を使って検索すれば、自分のプロジェクトのクラスパスが最優先で検索されます。

 ゲーム開発に限って言えばあまり関係ない気はするのですが、たまに自前で URLClassLoder を拡張してクラスパスをいじっているライブラリがあるので、そういうライブラリの ClassLoader を使うとリソースの検索パスの優先順位が意図しない状況になってたりします。そんな時にたまたま同名のリソースがあったりすると、目も当てられないことになります。一方で外部 jar 盛り盛りでもブートストラップの ClassLoader しか使ってなければ問題なかったりもするので、この辺はどんな環境なのか(どんな ClassLoder を使っているか)に依存します。そういう面倒くさいことを考えなくて済むように、自分のプロジェクトの(ブートストラップの) ClassLoaderを使いましょうという感じです。

終わりですー

ついに画像を表示できるようになったので、そろそろ 2D ゲームに関するグラフィクス処理は大詰めです。もうそろそろドラクエ6くらいまで作れるようになります。やや、もちろん技術的にはってことですよ。ゲーム作りの肝は圧倒的に画像や音楽を作るのとゲームバランスの調整ですので。

次回はアルファブレンド part2 として、ゲームの演出には欠かせない透過画像の表示やさまざまな合成方法をやってみたいと思います。
  LWJGL  コメント (0)  2012/03/15 19:55:04


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2019, 3>>
242526272812
3456789
10111213141516
17181920212223
24252627282930
31123456