All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.badlogic.gdx.graphics.glutils.KTXTextureData Maven / Gradle / Ivy

The newest version!

package com.badlogic.gdx.graphics.glutils;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.zip.GZIPInputStream;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Cubemap;
import com.badlogic.gdx.graphics.CubemapData;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.TextureData;
import com.badlogic.gdx.graphics.glutils.ETC1.ETC1Data;
import com.badlogic.gdx.utils.BufferUtils;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.StreamUtils;

/** A KTXTextureData holds the data from a KTX (or zipped KTX file, aka ZKTX). That is to say an OpenGL ready texture data. The
 * KTX file format is just a thin wrapper around OpenGL textures and therefore is compatible with most OpenGL texture capabilities
 * like texture compression, cubemapping, mipmapping, etc.
 *
 * For example, KTXTextureData can be used for {@link Texture} or {@link Cubemap}.
 *
 * @author Vincent Bousquet */
public class KTXTextureData implements TextureData, CubemapData {

	// The file we are loading
	private FileHandle file;

	// KTX header (only available after preparing)
	private int glType;
	private int glTypeSize;
	private int glFormat;
	private int glInternalFormat;
	private int glBaseInternalFormat;
	private int pixelWidth = -1;
	private int pixelHeight = -1;
	private int pixelDepth = -1;
	private int numberOfArrayElements;
	private int numberOfFaces;
	private int numberOfMipmapLevels;
	private int imagePos;

	// KTX image data (only available after preparing and before consuming)
	private ByteBuffer compressedData;

	// Whether to generate mipmaps if they are not included in the file
	private boolean useMipMaps;

	public KTXTextureData (FileHandle file, boolean genMipMaps) {
		this.file = file;
		this.useMipMaps = genMipMaps;
	}

	@Override
	public TextureDataType getType () {
		return TextureDataType.Custom;
	}

	@Override
	public boolean isPrepared () {
		return compressedData != null;
	}

	@Override
	public void prepare () {
		if (compressedData != null) throw new GdxRuntimeException("Already prepared");
		if (file == null) throw new GdxRuntimeException("Need a file to load from");
		// We support normal ktx files as well as 'zktx' which are gzip ktx file with an int length at the beginning (like ETC1).
		if (file.name().endsWith(".zktx")) {
			byte[] buffer = new byte[1024 * 10];
			DataInputStream in = null;
			try {
				in = new DataInputStream(new BufferedInputStream(new GZIPInputStream(file.read())));
				int fileSize = in.readInt();
				compressedData = BufferUtils.newUnsafeByteBuffer(fileSize);
				int readBytes = 0;
				while ((readBytes = in.read(buffer)) != -1)
					compressedData.put(buffer, 0, readBytes);
				((Buffer)compressedData).position(0);
				((Buffer)compressedData).limit(compressedData.capacity());
			} catch (Exception e) {
				throw new GdxRuntimeException("Couldn't load zktx file '" + file + "'", e);
			} finally {
				StreamUtils.closeQuietly(in);
			}
		} else {
			compressedData = ByteBuffer.wrap(file.readBytes());
		}
		if (compressedData.get() != (byte)0x0AB) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x04B) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x054) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x058) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x020) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x031) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x031) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x0BB) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x00D) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x00A) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x01A) throw new GdxRuntimeException("Invalid KTX Header");
		if (compressedData.get() != (byte)0x00A) throw new GdxRuntimeException("Invalid KTX Header");
		int endianTag = compressedData.getInt();
		if (endianTag != 0x04030201 && endianTag != 0x01020304) throw new GdxRuntimeException("Invalid KTX Header");
		if (endianTag != 0x04030201)
			compressedData.order(compressedData.order() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
		glType = compressedData.getInt();
		glTypeSize = compressedData.getInt();
		glFormat = compressedData.getInt();
		glInternalFormat = compressedData.getInt();
		glBaseInternalFormat = compressedData.getInt();
		pixelWidth = compressedData.getInt();
		pixelHeight = compressedData.getInt();
		pixelDepth = compressedData.getInt();
		numberOfArrayElements = compressedData.getInt();
		numberOfFaces = compressedData.getInt();
		numberOfMipmapLevels = compressedData.getInt();
		if (numberOfMipmapLevels == 0) {
			numberOfMipmapLevels = 1;
			useMipMaps = true;
		}
		int bytesOfKeyValueData = compressedData.getInt();
		imagePos = compressedData.position() + bytesOfKeyValueData;
		if (!compressedData.isDirect()) {
			int pos = imagePos;
			for (int level = 0; level < numberOfMipmapLevels; level++) {
				int faceLodSize = compressedData.getInt(pos);
				int faceLodSizeRounded = (faceLodSize + 3) & ~3;
				pos += faceLodSizeRounded * numberOfFaces + 4;
			}
			((Buffer)compressedData).limit(pos);
			((Buffer)compressedData).position(0);
			ByteBuffer directBuffer = BufferUtils.newUnsafeByteBuffer(pos);
			directBuffer.order(compressedData.order());
			directBuffer.put(compressedData);
			compressedData = directBuffer;
		}
	}

	private static final int GL_TEXTURE_1D = 0x1234;
	private static final int GL_TEXTURE_3D = 0x1234;
	private static final int GL_TEXTURE_1D_ARRAY_EXT = 0x1234;
	private static final int GL_TEXTURE_2D_ARRAY_EXT = 0x1234;

	@Override
	public void consumeCubemapData () {
		consumeCustomData(GL20.GL_TEXTURE_CUBE_MAP);
	}

	@Override
	public void consumeCustomData (int target) {
		if (compressedData == null) throw new GdxRuntimeException("Call prepare() before calling consumeCompressedData()");
		IntBuffer buffer = BufferUtils.newIntBuffer(16);

		// Check OpenGL type and format, detect compressed data format (no type & format)
		boolean compressed = false;
		if (glType == 0 || glFormat == 0) {
			if (glType + glFormat != 0) throw new GdxRuntimeException("either both or none of glType, glFormat must be zero");
			compressed = true;
		}

		// find OpenGL texture target and dimensions
		int textureDimensions = 1;
		int glTarget = GL_TEXTURE_1D;
		if (pixelHeight > 0) {
			textureDimensions = 2;
			glTarget = GL20.GL_TEXTURE_2D;
		}
		if (pixelDepth > 0) {
			textureDimensions = 3;
			glTarget = GL_TEXTURE_3D;
		}
		if (numberOfFaces == 6) {
			if (textureDimensions == 2)
				glTarget = GL20.GL_TEXTURE_CUBE_MAP;
			else
				throw new GdxRuntimeException("cube map needs 2D faces");
		} else if (numberOfFaces != 1) {
			throw new GdxRuntimeException("numberOfFaces must be either 1 or 6");
		}
		if (numberOfArrayElements > 0) {
			if (glTarget == GL_TEXTURE_1D)
				glTarget = GL_TEXTURE_1D_ARRAY_EXT;
			else if (glTarget == GL20.GL_TEXTURE_2D)
				glTarget = GL_TEXTURE_2D_ARRAY_EXT;
			else
				throw new GdxRuntimeException("No API for 3D and cube arrays yet");
			textureDimensions++;
		}
		if (glTarget == 0x1234)
			throw new GdxRuntimeException("Unsupported texture format (only 2D texture are supported in LibGdx for the time being)");

		int singleFace = -1;
		if (numberOfFaces == 6 && target != GL20.GL_TEXTURE_CUBE_MAP) {
			// Load a single face of the cube (should be avoided since the data is unloaded afterwards)
			if (!(GL20.GL_TEXTURE_CUBE_MAP_POSITIVE_X <= target && target <= GL20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z))
				throw new GdxRuntimeException(
					"You must specify either GL_TEXTURE_CUBE_MAP to bind all 6 faces of the cube or the requested face GL_TEXTURE_CUBE_MAP_POSITIVE_X and followings.");
			singleFace = target - GL20.GL_TEXTURE_CUBE_MAP_POSITIVE_X;
			target = GL20.GL_TEXTURE_CUBE_MAP_POSITIVE_X;
		} else if (numberOfFaces == 6 && target == GL20.GL_TEXTURE_CUBE_MAP) {
			// Load the 6 faces
			target = GL20.GL_TEXTURE_CUBE_MAP_POSITIVE_X;
		} else {
			// Load normal texture
			if (target != glTarget && !(GL20.GL_TEXTURE_CUBE_MAP_POSITIVE_X <= target
				&& target <= GL20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z && target == GL20.GL_TEXTURE_2D))
				throw new GdxRuntimeException("Invalid target requested : 0x" + Integer.toHexString(target) + ", expecting : 0x"
					+ Integer.toHexString(glTarget));
		}

		// KTX files require an unpack alignment of 4
		Gdx.gl.glGetIntegerv(GL20.GL_UNPACK_ALIGNMENT, buffer);
		int previousUnpackAlignment = buffer.get(0);
		if (previousUnpackAlignment != 4) Gdx.gl.glPixelStorei(GL20.GL_UNPACK_ALIGNMENT, 4);
		int glInternalFormat = this.glInternalFormat;
		int glFormat = this.glFormat;
		int pos = imagePos;
		for (int level = 0; level < numberOfMipmapLevels; level++) {
			int pixelWidth = Math.max(1, this.pixelWidth >> level);
			int pixelHeight = Math.max(1, this.pixelHeight >> level);
			int pixelDepth = Math.max(1, this.pixelDepth >> level);
			((Buffer)compressedData).position(pos);
			int faceLodSize = compressedData.getInt();
			int faceLodSizeRounded = (faceLodSize + 3) & ~3;
			pos += 4;
			for (int face = 0; face < numberOfFaces; face++) {
				((Buffer)compressedData).position(pos);
				pos += faceLodSizeRounded;
				if (singleFace != -1 && singleFace != face) continue;
				ByteBuffer data = compressedData.slice();
				((Buffer)data).limit(faceLodSizeRounded);
				if (textureDimensions == 1) {
					// if (compressed)
					// Gdx.gl.glCompressedTexImage1D(target + face, level, glInternalFormat, pixelWidth, 0, faceLodSize,
					// data);
					// else
					// Gdx.gl.glTexImage1D(target + face, level, glInternalFormat, pixelWidth, 0, glFormat, glType, data);
				} else if (textureDimensions == 2) {
					if (numberOfArrayElements > 0) pixelHeight = numberOfArrayElements;
					if (compressed) {
						if (glInternalFormat == ETC1.ETC1_RGB8_OES) {
							if (!Gdx.graphics.supportsExtension("GL_OES_compressed_ETC1_RGB8_texture")) {
								ETC1Data etcData = new ETC1Data(pixelWidth, pixelHeight, data, 0);
								Pixmap pixmap = ETC1.decodeImage(etcData, Format.RGB888);
								Gdx.gl.glTexImage2D(target + face, level, pixmap.getGLInternalFormat(), pixmap.getWidth(),
									pixmap.getHeight(), 0, pixmap.getGLFormat(), pixmap.getGLType(), pixmap.getPixels());
								pixmap.dispose();
							} else {
								Gdx.gl.glCompressedTexImage2D(target + face, level, glInternalFormat, pixelWidth, pixelHeight, 0,
									faceLodSize, data);
							}
						} else {
							// Try to load (no software unpacking fallback)
							Gdx.gl.glCompressedTexImage2D(target + face, level, glInternalFormat, pixelWidth, pixelHeight, 0,
								faceLodSize, data);
						}
					} else
						Gdx.gl.glTexImage2D(target + face, level, glInternalFormat, pixelWidth, pixelHeight, 0, glFormat, glType, data);
				} else if (textureDimensions == 3) {
					if (numberOfArrayElements > 0) pixelDepth = numberOfArrayElements;
					// if (compressed)
					// Gdx.gl.glCompressedTexImage3D(target + face, level, glInternalFormat, pixelWidth, pixelHeight, pixelDepth, 0,
					// faceLodSize, data);
					// else
					// Gdx.gl.glTexImage3D(target + face, level, glInternalFormat, pixelWidth, pixelHeight, pixelDepth, 0, glFormat,
					// glType, data);
				}
			}
		}
		if (previousUnpackAlignment != 4) Gdx.gl.glPixelStorei(GL20.GL_UNPACK_ALIGNMENT, previousUnpackAlignment);
		if (useMipMaps()) Gdx.gl.glGenerateMipmap(target);

		// dispose data once transfered to GPU
		disposePreparedData();
	}

	public void disposePreparedData () {
		if (compressedData != null) BufferUtils.disposeUnsafeByteBuffer(compressedData);
		compressedData = null;
	}

	@Override
	public Pixmap consumePixmap () {
		throw new GdxRuntimeException("This TextureData implementation does not return a Pixmap");
	}

	@Override
	public boolean disposePixmap () {
		throw new GdxRuntimeException("This TextureData implementation does not return a Pixmap");
	}

	@Override
	public int getWidth () {
		return pixelWidth;
	}

	@Override
	public int getHeight () {
		return pixelHeight;
	}

	public int getNumberOfMipMapLevels () {
		return numberOfMipmapLevels;
	}

	public int getNumberOfFaces () {
		return numberOfFaces;
	}

	public int getGlInternalFormat () {
		return glInternalFormat;
	}

	public ByteBuffer getData (int requestedLevel, int requestedFace) {
		int pos = imagePos;
		for (int level = 0; level < numberOfMipmapLevels; level++) {
			int faceLodSize = compressedData.getInt(pos);
			int faceLodSizeRounded = (faceLodSize + 3) & ~3;
			pos += 4;
			if (level == requestedLevel) {
				for (int face = 0; face < numberOfFaces; face++) {
					if (face == requestedFace) {
						((Buffer)compressedData).position(pos);
						ByteBuffer data = compressedData.slice();
						((Buffer)data).limit(faceLodSizeRounded);
						return data;
					}
					pos += faceLodSizeRounded;
				}
			} else {
				pos += faceLodSizeRounded * numberOfFaces;
			}
		}
		return null;
	}

	@Override
	public Format getFormat () {
		throw new GdxRuntimeException("This TextureData implementation directly handles texture formats.");
	}

	@Override
	public boolean useMipMaps () {
		return useMipMaps;
	}

	@Override
	public boolean isManaged () {
		return true;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy