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

com.badlogic.gdx.tools.ktx.KTXProcessor Maven / Gradle / Ivy

The newest version!

package com.badlogic.gdx.tools.ktx;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.zip.GZIPOutputStream;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.headless.HeadlessApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglNativesLoader;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Blending;
import com.badlogic.gdx.graphics.Pixmap.Filter;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.glutils.ETC1;
import com.badlogic.gdx.graphics.glutils.ETC1.ETC1Data;
import com.badlogic.gdx.graphics.glutils.KTXTextureData;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;

public class KTXProcessor {

	final static byte[] HEADER_MAGIC = {(byte)0x0AB, (byte)0x04B, (byte)0x054, (byte)0x058, (byte)0x020, (byte)0x031, (byte)0x031,
		(byte)0x0BB, (byte)0x00D, (byte)0x00A, (byte)0x01A, (byte)0x00A};

	public static void convert (String input, String output, boolean genMipmaps, boolean packETC1, boolean genAlphaAtlas)
		throws Exception {
		Array opts = new Array(String.class);
		opts.add(input);
		opts.add(output);
		if (genMipmaps) opts.add("-mipmaps");
		if (packETC1 && !genAlphaAtlas) opts.add("-etc1");
		if (packETC1 && genAlphaAtlas) opts.add("-etc1a");
		main(opts.toArray());
	}

	public static void convert (String inPx, String inNx, String inPy, String inNy, String inPz, String inNz, String output,
		boolean genMipmaps, boolean packETC1, boolean genAlphaAtlas) throws Exception {
		Array opts = new Array(String.class);
		opts.add(inPx);
		opts.add(inNx);
		opts.add(inPy);
		opts.add(inNy);
		opts.add(inPz);
		opts.add(inNz);
		opts.add(output);
		if (genMipmaps) opts.add("-mipmaps");
		if (packETC1 && !genAlphaAtlas) opts.add("-etc1");
		if (packETC1 && genAlphaAtlas) opts.add("-etc1a");
		main(opts.toArray());
	}

	private final static int DISPOSE_DONT = 0;
	private final static int DISPOSE_PACK = 1;
	private final static int DISPOSE_FACE = 2;
	private final static int DISPOSE_LEVEL = 4;

	public static void main (String[] args) {
		new HeadlessApplication(new KTXProcessorListener(args));
	}

	public static class KTXProcessorListener extends ApplicationAdapter {
		String[] args;

		KTXProcessorListener (String[] args) {
			this.args = args;
		}

		@Override
		public void create () {
			boolean isCubemap = args.length == 7 || args.length == 8 || args.length == 9;
			boolean isTexture = args.length == 2 || args.length == 3 || args.length == 4;
			boolean isPackETC1 = false, isAlphaAtlas = false, isGenMipMaps = false;
			if (!isCubemap && !isTexture) {
				System.out.println("usage : KTXProcessor input_file output_file [-etc1|-etc1a] [-mipmaps]");
				System.out.println("  input_file  is the texture file to include in the output KTX or ZKTX file.");
				System.out.println(
					"              for cube map, just provide 6 input files corresponding to the faces in the following order : X+, X-, Y+, Y-, Z+, Z-");
				System.out.println(
					"  output_file is the path to the output file, its type is based on the extension which must be either KTX or ZKTX");
				System.out.println();
				System.out.println("  options:");
				System.out.println("    -etc1    input file will be packed using ETC1 compression, dropping the alpha channel");
				System.out.println(
					"    -etc1a   input file will be packed using ETC1 compression, doubling the height and placing the alpha channel in the bottom half");
				System.out.println("    -mipmaps input file will be processed to generate mipmaps");
				System.out.println();
				System.out.println("  examples:");
				System.out.println(
					"    KTXProcessor in.png out.ktx                                        Create a KTX file with the provided 2D texture");
				System.out.println(
					"    KTXProcessor in.png out.zktx                                       Create a Zipped KTX file with the provided 2D texture");
				System.out.println(
					"    KTXProcessor in.png out.zktx -mipmaps                              Create a Zipped KTX file with the provided 2D texture, generating all mipmap levels");
				System.out.println(
					"    KTXProcessor px.ktx nx.ktx py.ktx ny.ktx pz.ktx nz.ktx out.zktx    Create a Zipped KTX file with the provided cubemap textures");
				System.out.println(
					"    KTXProcessor in.ktx out.zktx                                       Convert a KTX file to a Zipped KTX file");
				System.exit(-1);
			}

			LwjglNativesLoader.load();

			// Loads other options
			for (int i = 0; i < args.length; i++) {
				System.out.println(i + " = " + args[i]);
				if (isTexture && i < 2) continue;
				if (isCubemap && i < 7) continue;
				if ("-etc1".equals(args[i])) isPackETC1 = true;
				if ("-etc1a".equals(args[i])) isAlphaAtlas = isPackETC1 = true;
				if ("-mipmaps".equals(args[i])) isGenMipMaps = true;
			}

			File output = new File(args[isCubemap ? 6 : 1]);

			// Check if we have a cubemapped ktx file as input
			int ktxDispose = DISPOSE_DONT;
			KTXTextureData ktx = null;
			FileHandle file = new FileHandle(args[0]);
			if (file.name().toLowerCase().endsWith(".ktx") || file.name().toLowerCase().endsWith(".zktx")) {
				ktx = new KTXTextureData(file, false);
				if (ktx.getNumberOfFaces() == 6) isCubemap = true;
				ktxDispose = DISPOSE_PACK;
			}

			// Process all faces
			int nFaces = isCubemap ? 6 : 1;
			Image[][] images = new Image[nFaces][];
			int texWidth = -1, texHeight = -1, texFormat = -1, nLevels = 0;
			for (int face = 0; face < nFaces; face++) {
				ETC1Data etc1 = null;
				Pixmap facePixmap = null;
				int ktxFace = 0;

				// Load source image (ends up with either ktx, etc1 or facePixmap initialized)
				if (ktx != null && ktx.getNumberOfFaces() == 6) {
					// No loading since we have a ktx file with cubemap as input
					nLevels = ktx.getNumberOfMipMapLevels();
					ktxFace = face;
				} else {
					file = new FileHandle(args[face]);
					System.out.println("Processing : " + file + " for face #" + face);
					if (file.name().toLowerCase().endsWith(".ktx") || file.name().toLowerCase().endsWith(".zktx")) {
						if (ktx == null || ktx.getNumberOfFaces() != 6) {
							ktxDispose = DISPOSE_FACE;
							ktx = new KTXTextureData(file, false);
							ktx.prepare();
						}
						nLevels = ktx.getNumberOfMipMapLevels();
						texWidth = ktx.getWidth();
						texHeight = ktx.getHeight();
					} else if (file.name().toLowerCase().endsWith(".etc1")) {
						etc1 = new ETC1Data(file);
						nLevels = 1;
						texWidth = etc1.width;
						texHeight = etc1.height;
					} else {
						facePixmap = new Pixmap(file);
						facePixmap.setBlending(Blending.None);
						facePixmap.setFilter(Filter.BiLinear);
						nLevels = 1;
						texWidth = facePixmap.getWidth();
						texHeight = facePixmap.getHeight();
					}
					if (isGenMipMaps) {
						if (!MathUtils.isPowerOfTwo(texWidth) || !MathUtils.isPowerOfTwo(texHeight)) throw new GdxRuntimeException(
							"Invalid input : mipmap generation is only available for power of two textures : " + file);
						nLevels = Math.max(Integer.SIZE - Integer.numberOfLeadingZeros(texWidth),
							Integer.SIZE - Integer.numberOfLeadingZeros(texHeight));
					}
				}

				// Process each mipmap level
				images[face] = new Image[nLevels];
				for (int level = 0; level < nLevels; level++) {
					int levelWidth = Math.max(1, texWidth >> level);
					int levelHeight = Math.max(1, texHeight >> level);

					// Get pixmap for this level (ends with either levelETCData or levelPixmap being non null)
					Pixmap levelPixmap = null;
					ETC1Data levelETCData = null;
					if (ktx != null) {
						ByteBuffer ktxData = ktx.getData(level, ktxFace);
						if (ktxData != null && ktx.getGlInternalFormat() == ETC1.ETC1_RGB8_OES)
							levelETCData = new ETC1Data(levelWidth, levelHeight, ktxData, 0);
					}
					if (ktx != null && levelETCData == null && facePixmap == null) {
						ByteBuffer ktxData = ktx.getData(0, ktxFace);
						if (ktxData != null && ktx.getGlInternalFormat() == ETC1.ETC1_RGB8_OES)
							facePixmap = ETC1.decodeImage(new ETC1Data(levelWidth, levelHeight, ktxData, 0), Format.RGB888);
					}
					if (level == 0 && etc1 != null) {
						levelETCData = etc1;
					}
					if (levelETCData == null && etc1 != null && facePixmap == null) {
						facePixmap = ETC1.decodeImage(etc1, Format.RGB888);
					}
					if (levelETCData == null) {
						levelPixmap = new Pixmap(levelWidth, levelHeight, facePixmap.getFormat());
						levelPixmap.setBlending(Blending.None);
						levelPixmap.setFilter(Filter.BiLinear);
						levelPixmap.drawPixmap(facePixmap, 0, 0, facePixmap.getWidth(), facePixmap.getHeight(), 0, 0,
							levelPixmap.getWidth(), levelPixmap.getHeight());
					}
					if (levelETCData == null && levelPixmap == null)
						throw new GdxRuntimeException("Failed to load data for face " + face + " / mipmap level " + level);

					// Create alpha atlas
					if (isAlphaAtlas) {
						if (levelPixmap == null) levelPixmap = ETC1.decodeImage(levelETCData, Format.RGB888);
						int w = levelPixmap.getWidth(), h = levelPixmap.getHeight();
						Pixmap pm = new Pixmap(w, h * 2, levelPixmap.getFormat());
						pm.setBlending(Blending.None);
						pm.setFilter(Filter.BiLinear);
						pm.drawPixmap(levelPixmap, 0, 0);
						for (int y = 0; y < h; y++) {
							for (int x = 0; x < w; x++) {
								int alpha = (levelPixmap.getPixel(x, y)) & 0x0FF;
								pm.drawPixel(x, y + h, (alpha << 24) | (alpha << 16) | (alpha << 8) | 0x0FF);
							}
						}
						levelPixmap.dispose();
						levelPixmap = pm;
						levelETCData = null;
					}

					// Perform ETC1 compression
					if (levelETCData == null && isPackETC1) {
						if (levelPixmap.getFormat() != Format.RGB888 && levelPixmap.getFormat() != Format.RGB565) {
							if (!isAlphaAtlas)
								System.out.println("Converting from " + levelPixmap.getFormat() + " to RGB888 for ETC1 compression");
							Pixmap tmp = new Pixmap(levelPixmap.getWidth(), levelPixmap.getHeight(), Format.RGB888);
							tmp.setBlending(Blending.None);
							tmp.setFilter(Filter.BiLinear);
							tmp.drawPixmap(levelPixmap, 0, 0, 0, 0, levelPixmap.getWidth(), levelPixmap.getHeight());
							levelPixmap.dispose();
							levelPixmap = tmp;
						}
						// System.out.println("Compress : " + levelWidth + " x " + levelHeight);
						levelETCData = ETC1.encodeImagePKM(levelPixmap);
						levelPixmap.dispose();
						levelPixmap = null;
					}

					// Save result to ouput ktx
					images[face][level] = new Image();
					images[face][level].etcData = levelETCData;
					images[face][level].pixmap = levelPixmap;
					if (levelPixmap != null) {
						levelPixmap.dispose();
						facePixmap = null;
					}
				}

				// Dispose resources
				if (facePixmap != null) {
					facePixmap.dispose();
					facePixmap = null;
				}
				if (etc1 != null) {
					etc1.dispose();
					etc1 = null;
				}
				if (ktx != null && ktxDispose == DISPOSE_FACE) {
					ktx.disposePreparedData();
					ktx = null;
				}
			}
			if (ktx != null) {
				ktx.disposePreparedData();
				ktx = null;
			}

			int glType, glTypeSize, glFormat, glInternalFormat, glBaseInternalFormat;
			if (isPackETC1) {
				glType = glFormat = 0;
				glTypeSize = 1;
				glInternalFormat = ETC1.ETC1_RGB8_OES;
				glBaseInternalFormat = GL20.GL_RGB;
			} else if (images[0][0].pixmap != null) {
				glType = images[0][0].pixmap.getGLType();
				glTypeSize = 1;
				glFormat = images[0][0].pixmap.getGLFormat();
				glInternalFormat = images[0][0].pixmap.getGLInternalFormat();
				glBaseInternalFormat = glFormat;
			} else
				throw new GdxRuntimeException("Unsupported output format");

			int totalSize = 12 + 13 * 4;
			for (int level = 0; level < nLevels; level++) {
				System.out.println("Level: " + level);
				int faceLodSize = images[0][level].getSize();
				int faceLodSizeRounded = (faceLodSize + 3) & ~3;
				totalSize += 4;
				totalSize += nFaces * faceLodSizeRounded;
			}

			try {
				DataOutputStream out;
				if (output.getName().toLowerCase().endsWith(".zktx")) {
					out = new DataOutputStream(new GZIPOutputStream(new FileOutputStream(output)));
					out.writeInt(totalSize);
				} else
					out = new DataOutputStream(new FileOutputStream(output));

				out.write(HEADER_MAGIC);
				out.writeInt(0x04030201);
				out.writeInt(glType);
				out.writeInt(glTypeSize);
				out.writeInt(glFormat);
				out.writeInt(glInternalFormat);
				out.writeInt(glBaseInternalFormat);
				out.writeInt(texWidth);
				out.writeInt(isAlphaAtlas ? (2 * texHeight) : texHeight);
				out.writeInt(0); // depth (not supported)
				out.writeInt(0); // n array elements (not supported)
				out.writeInt(nFaces);
				out.writeInt(nLevels);
				out.writeInt(0); // No additional info (key/value pairs)
				for (int level = 0; level < nLevels; level++) {
					int faceLodSize = images[0][level].getSize();
					int faceLodSizeRounded = (faceLodSize + 3) & ~3;
					out.writeInt(faceLodSize);
					for (int face = 0; face < nFaces; face++) {
						byte[] bytes = images[face][level].getBytes();
						out.write(bytes);
						for (int j = bytes.length; j < faceLodSizeRounded; j++)
							out.write((byte)0x00);
					}
				}

				out.close();
				System.out.println("Finished");
			} catch (Exception e) {
				Gdx.app.error("KTXProcessor", "Error writing to file: " + output.getName(), e);
			}

			Gdx.app.exit();
		}
	}

	private static class Image {

		public ETC1Data etcData;
		public Pixmap pixmap;

		public Image () {
		}

		public int getSize () {
			if (etcData != null) return etcData.compressedData.limit() - etcData.dataOffset;
			throw new GdxRuntimeException("Unsupported output format, try adding '-etc1' as argument");
		}

		public byte[] getBytes () {
			if (etcData != null) {
				byte[] result = new byte[getSize()];
				((Buffer)etcData.compressedData).position(etcData.dataOffset);
				etcData.compressedData.get(result);
				return result;
			}
			throw new GdxRuntimeException("Unsupported output format, try adding '-etc1' as argument");
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy