Home | History | Annotate | Download | only in ktx
      1 
      2 package com.badlogic.gdx.tools.ktx;
      3 
      4 import java.io.DataOutputStream;
      5 import java.io.File;
      6 import java.io.FileOutputStream;
      7 import java.nio.ByteBuffer;
      8 import java.util.zip.GZIPOutputStream;
      9 
     10 import com.badlogic.gdx.ApplicationAdapter;
     11 import com.badlogic.gdx.Gdx;
     12 import com.badlogic.gdx.backends.headless.HeadlessApplication;
     13 import com.badlogic.gdx.backends.lwjgl.LwjglNativesLoader;
     14 import com.badlogic.gdx.files.FileHandle;
     15 import com.badlogic.gdx.graphics.GL20;
     16 import com.badlogic.gdx.graphics.Pixmap;
     17 import com.badlogic.gdx.graphics.Pixmap.Blending;
     18 import com.badlogic.gdx.graphics.Pixmap.Filter;
     19 import com.badlogic.gdx.graphics.Pixmap.Format;
     20 import com.badlogic.gdx.graphics.glutils.ETC1;
     21 import com.badlogic.gdx.graphics.glutils.ETC1.ETC1Data;
     22 import com.badlogic.gdx.graphics.glutils.KTXTextureData;
     23 import com.badlogic.gdx.math.MathUtils;
     24 import com.badlogic.gdx.utils.Array;
     25 import com.badlogic.gdx.utils.GdxRuntimeException;
     26 
     27 public class KTXProcessor {
     28 
     29 	final static byte[] HEADER_MAGIC = {(byte)0x0AB, (byte)0x04B, (byte)0x054, (byte)0x058, (byte)0x020, (byte)0x031,
     30 		(byte)0x031, (byte)0x0BB, (byte)0x00D, (byte)0x00A, (byte)0x01A, (byte)0x00A};
     31 
     32 	public static void convert (String input, String output, boolean genMipmaps, boolean packETC1, boolean genAlphaAtlas)
     33 		throws Exception {
     34 		Array<String> opts = new Array<String>(String.class);
     35 		opts.add(input);
     36 		opts.add(output);
     37 		if (genMipmaps) opts.add("-mipmaps");
     38 		if (packETC1 && !genAlphaAtlas) opts.add("-etc1");
     39 		if (packETC1 && genAlphaAtlas) opts.add("-etc1a");
     40 		main(opts.toArray());
     41 	}
     42 
     43 	public static void convert (String inPx, String inNx, String inPy, String inNy, String inPz, String inNz, String output,
     44 		boolean genMipmaps, boolean packETC1, boolean genAlphaAtlas) throws Exception {
     45 		Array<String> opts = new Array<String>(String.class);
     46 		opts.add(inPx);
     47 		opts.add(inNx);
     48 		opts.add(inPy);
     49 		opts.add(inNy);
     50 		opts.add(inPz);
     51 		opts.add(inNz);
     52 		opts.add(output);
     53 		if (genMipmaps) opts.add("-mipmaps");
     54 		if (packETC1 && !genAlphaAtlas) opts.add("-etc1");
     55 		if (packETC1 && genAlphaAtlas) opts.add("-etc1a");
     56 		main(opts.toArray());
     57 	}
     58 
     59 	private final static int DISPOSE_DONT = 0;
     60 	private final static int DISPOSE_PACK = 1;
     61 	private final static int DISPOSE_FACE = 2;
     62 	private final static int DISPOSE_LEVEL = 4;
     63 
     64 	public static void main (String[] args) {
     65 		new HeadlessApplication(new KTXProcessorListener(args));
     66 	}
     67 
     68 	public static class KTXProcessorListener extends ApplicationAdapter {
     69 		String[] args;
     70 
     71 		KTXProcessorListener (String[] args) {
     72 			this.args = args;
     73 		}
     74 
     75 		@Override
     76 		public void create () {
     77 			boolean isCubemap = args.length == 7 || args.length == 8 || args.length == 9;
     78 			boolean isTexture = args.length == 2 || args.length == 3 || args.length == 4;
     79 			boolean isPackETC1 = false, isAlphaAtlas = false, isGenMipMaps = false;
     80 			if (!isCubemap && !isTexture) {
     81 				System.out.println("usage : KTXProcessor input_file output_file [-etc1|-etc1a] [-mipmaps]");
     82 				System.out.println("  input_file  is the texture file to include in the output KTX or ZKTX file.");
     83 				System.out
     84 					.println("              for cube map, just provide 6 input files corresponding to the faces in the following order : X+, X-, Y+, Y-, Z+, Z-");
     85 				System.out
     86 					.println("  output_file is the path to the output file, its type is based on the extension which must be either KTX or ZKTX");
     87 				System.out.println();
     88 				System.out.println("  options:");
     89 				System.out.println("    -etc1    input file will be packed using ETC1 compression, dropping the alpha channel");
     90 				System.out
     91 					.println("    -etc1a   input file will be packed using ETC1 compression, doubling the height and placing the alpha channel in the bottom half");
     92 				System.out.println("    -mipmaps input file will be processed to generate mipmaps");
     93 				System.out.println();
     94 				System.out.println("  examples:");
     95 				System.out
     96 					.println("    KTXProcessor in.png out.ktx                                        Create a KTX file with the provided 2D texture");
     97 				System.out
     98 					.println("    KTXProcessor in.png out.zktx                                       Create a Zipped KTX file with the provided 2D texture");
     99 				System.out
    100 					.println("    KTXProcessor in.png out.zktx -mipmaps                              Create a Zipped KTX file with the provided 2D texture, generating all mipmap levels");
    101 				System.out
    102 					.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");
    103 				System.out
    104 					.println("    KTXProcessor in.ktx out.zktx                                       Convert a KTX file to a Zipped KTX file");
    105 				System.exit(-1);
    106 			}
    107 
    108 			LwjglNativesLoader.load();
    109 
    110 			// Loads other options
    111 			for (int i = 0; i < args.length; i++) {
    112 				System.out.println(i + " = " + args[i]);
    113 				if (isTexture && i < 2) continue;
    114 				if (isCubemap && i < 7) continue;
    115 				if ("-etc1".equals(args[i])) isPackETC1 = true;
    116 				if ("-etc1a".equals(args[i])) isAlphaAtlas = isPackETC1 = true;
    117 				if ("-mipmaps".equals(args[i])) isGenMipMaps = true;
    118 			}
    119 
    120 			File output = new File(args[isCubemap ? 6 : 1]);
    121 
    122 			// Check if we have a cubemapped ktx file as input
    123 			int ktxDispose = DISPOSE_DONT;
    124 			KTXTextureData ktx = null;
    125 			FileHandle file = new FileHandle(args[0]);
    126 			if (file.name().toLowerCase().endsWith(".ktx") || file.name().toLowerCase().endsWith(".zktx")) {
    127 				ktx = new KTXTextureData(file, false);
    128 				if (ktx.getNumberOfFaces() == 6) isCubemap = true;
    129 				ktxDispose = DISPOSE_PACK;
    130 			}
    131 
    132 			// Process all faces
    133 			int nFaces = isCubemap ? 6 : 1;
    134 			Image[][] images = new Image[nFaces][];
    135 			Pixmap.setBlending(Blending.None);
    136 			Pixmap.setFilter(Filter.BiLinear);
    137 			int texWidth = -1, texHeight = -1, texFormat = -1, nLevels = 0;
    138 			for (int face = 0; face < nFaces; face++) {
    139 				ETC1Data etc1 = null;
    140 				Pixmap facePixmap = null;
    141 				int ktxFace = 0;
    142 
    143 				// Load source image (ends up with either ktx, etc1 or facePixmap initialized)
    144 				if (ktx != null && ktx.getNumberOfFaces() == 6) {
    145 					// No loading since we have a ktx file with cubemap as input
    146 					nLevels = ktx.getNumberOfMipMapLevels();
    147 					ktxFace = face;
    148 				} else {
    149 					file = new FileHandle(args[face]);
    150 					System.out.println("Processing : " + file + " for face #" + face);
    151 					if (file.name().toLowerCase().endsWith(".ktx") || file.name().toLowerCase().endsWith(".zktx")) {
    152 						if (ktx == null || ktx.getNumberOfFaces() != 6) {
    153 							ktxDispose = DISPOSE_FACE;
    154 							ktx = new KTXTextureData(file, false);
    155 							ktx.prepare();
    156 						}
    157 						nLevels = ktx.getNumberOfMipMapLevels();
    158 						texWidth = ktx.getWidth();
    159 						texHeight = ktx.getHeight();
    160 					} else if (file.name().toLowerCase().endsWith(".etc1")) {
    161 						etc1 = new ETC1Data(file);
    162 						nLevels = 1;
    163 						texWidth = etc1.width;
    164 						texHeight = etc1.height;
    165 					} else {
    166 						facePixmap = new Pixmap(file);
    167 						nLevels = 1;
    168 						texWidth = facePixmap.getWidth();
    169 						texHeight = facePixmap.getHeight();
    170 					}
    171 					if (isGenMipMaps) {
    172 						if (!MathUtils.isPowerOfTwo(texWidth) || !MathUtils.isPowerOfTwo(texHeight))
    173 							throw new GdxRuntimeException(
    174 								"Invalid input : mipmap generation is only available for power of two textures : " + file);
    175 						nLevels = Math.max(Integer.SIZE - Integer.numberOfLeadingZeros(texWidth),
    176 							Integer.SIZE - Integer.numberOfLeadingZeros(texHeight));
    177 					}
    178 				}
    179 
    180 				// Process each mipmap level
    181 				images[face] = new Image[nLevels];
    182 				for (int level = 0; level < nLevels; level++) {
    183 					int levelWidth = Math.max(1, texWidth >> level);
    184 					int levelHeight = Math.max(1, texHeight >> level);
    185 
    186 					// Get pixmap for this level (ends with either levelETCData or levelPixmap being non null)
    187 					Pixmap levelPixmap = null;
    188 					ETC1Data levelETCData = null;
    189 					if (ktx != null) {
    190 						ByteBuffer ktxData = ktx.getData(level, ktxFace);
    191 						if (ktxData != null && ktx.getGlInternalFormat() == ETC1.ETC1_RGB8_OES)
    192 							levelETCData = new ETC1Data(levelWidth, levelHeight, ktxData, 0);
    193 					}
    194 					if (ktx != null && levelETCData == null && facePixmap == null) {
    195 						ByteBuffer ktxData = ktx.getData(0, ktxFace);
    196 						if (ktxData != null && ktx.getGlInternalFormat() == ETC1.ETC1_RGB8_OES)
    197 							facePixmap = ETC1.decodeImage(new ETC1Data(levelWidth, levelHeight, ktxData, 0), Format.RGB888);
    198 					}
    199 					if (level == 0 && etc1 != null) {
    200 						levelETCData = etc1;
    201 					}
    202 					if (levelETCData == null && etc1 != null && facePixmap == null) {
    203 						facePixmap = ETC1.decodeImage(etc1, Format.RGB888);
    204 					}
    205 					if (levelETCData == null) {
    206 						levelPixmap = new Pixmap(levelWidth, levelHeight, facePixmap.getFormat());
    207 						levelPixmap.drawPixmap(facePixmap, 0, 0, facePixmap.getWidth(), facePixmap.getHeight(), 0, 0,
    208 							levelPixmap.getWidth(), levelPixmap.getHeight());
    209 					}
    210 					if (levelETCData == null && levelPixmap == null)
    211 						throw new GdxRuntimeException("Failed to load data for face " + face + " / mipmap level " + level);
    212 
    213 					// Create alpha atlas
    214 					if (isAlphaAtlas) {
    215 						if (levelPixmap == null) levelPixmap = ETC1.decodeImage(levelETCData, Format.RGB888);
    216 						int w = levelPixmap.getWidth(), h = levelPixmap.getHeight();
    217 						Pixmap pm = new Pixmap(w, h * 2, levelPixmap.getFormat());
    218 						pm.drawPixmap(levelPixmap, 0, 0);
    219 						for (int y = 0; y < h; y++) {
    220 							for (int x = 0; x < w; x++) {
    221 								int alpha = (levelPixmap.getPixel(x, y)) & 0x0FF;
    222 								pm.drawPixel(x, y + h, (alpha << 24) | (alpha << 16) | (alpha << 8) | 0x0FF);
    223 							}
    224 						}
    225 						levelPixmap.dispose();
    226 						levelPixmap = pm;
    227 						levelETCData = null;
    228 					}
    229 
    230 					// Perform ETC1 compression
    231 					if (levelETCData == null && isPackETC1) {
    232 						if (levelPixmap.getFormat() != Format.RGB888 && levelPixmap.getFormat() != Format.RGB565) {
    233 							if (!isAlphaAtlas)
    234 								System.out.println("Converting from " + levelPixmap.getFormat() + " to RGB888 for ETC1 compression");
    235 							Pixmap tmp = new Pixmap(levelPixmap.getWidth(), levelPixmap.getHeight(), Format.RGB888);
    236 							tmp.drawPixmap(levelPixmap, 0, 0, 0, 0, levelPixmap.getWidth(), levelPixmap.getHeight());
    237 							levelPixmap.dispose();
    238 							levelPixmap = tmp;
    239 						}
    240 						// System.out.println("Compress : " + levelWidth + " x " + levelHeight);
    241 						levelETCData = ETC1.encodeImagePKM(levelPixmap);
    242 						levelPixmap.dispose();
    243 						levelPixmap = null;
    244 					}
    245 
    246 					// Save result to ouput ktx
    247 					images[face][level] = new Image();
    248 					images[face][level].etcData = levelETCData;
    249 					images[face][level].pixmap = levelPixmap;
    250 					if (levelPixmap != null) {
    251 						levelPixmap.dispose();
    252 						facePixmap = null;
    253 					}
    254 				}
    255 
    256 				// Dispose resources
    257 				if (facePixmap != null) {
    258 					facePixmap.dispose();
    259 					facePixmap = null;
    260 				}
    261 				if (etc1 != null) {
    262 					etc1.dispose();
    263 					etc1 = null;
    264 				}
    265 				if (ktx != null && ktxDispose == DISPOSE_FACE) {
    266 					ktx.disposePreparedData();
    267 					ktx = null;
    268 				}
    269 			}
    270 			if (ktx != null) {
    271 				ktx.disposePreparedData();
    272 				ktx = null;
    273 			}
    274 
    275 			int glType, glTypeSize, glFormat, glInternalFormat, glBaseInternalFormat;
    276 			if (isPackETC1) {
    277 				glType = glFormat = 0;
    278 				glTypeSize = 1;
    279 				glInternalFormat = ETC1.ETC1_RGB8_OES;
    280 				glBaseInternalFormat = GL20.GL_RGB;
    281 			} else if (images[0][0].pixmap != null) {
    282 				glType = images[0][0].pixmap.getGLType();
    283 				glTypeSize = 1;
    284 				glFormat = images[0][0].pixmap.getGLFormat();
    285 				glInternalFormat = images[0][0].pixmap.getGLInternalFormat();
    286 				glBaseInternalFormat = glFormat;
    287 			} else
    288 				throw new GdxRuntimeException("Unsupported output format");
    289 
    290 			int totalSize = 12 + 13 * 4;
    291 			for (int level = 0; level < nLevels; level++) {
    292 				System.out.println("Level: " + level);
    293 				int faceLodSize = images[0][level].getSize();
    294 				int faceLodSizeRounded = (faceLodSize + 3) & ~3;
    295 				totalSize += 4;
    296 				totalSize += nFaces * faceLodSizeRounded;
    297 			}
    298 
    299 			try {
    300 				DataOutputStream out;
    301 				if (output.getName().toLowerCase().endsWith(".zktx")) {
    302 					out = new DataOutputStream(new GZIPOutputStream(new FileOutputStream(output)));
    303 					out.writeInt(totalSize);
    304 				} else
    305 					out = new DataOutputStream(new FileOutputStream(output));
    306 
    307 				out.write(HEADER_MAGIC);
    308 				out.writeInt(0x04030201);
    309 				out.writeInt(glType);
    310 				out.writeInt(glTypeSize);
    311 				out.writeInt(glFormat);
    312 				out.writeInt(glInternalFormat);
    313 				out.writeInt(glBaseInternalFormat);
    314 				out.writeInt(texWidth);
    315 				out.writeInt(isAlphaAtlas ? (2 * texHeight) : texHeight);
    316 				out.writeInt(0); // depth (not supported)
    317 				out.writeInt(0); // n array elements (not supported)
    318 				out.writeInt(nFaces);
    319 				out.writeInt(nLevels);
    320 				out.writeInt(0); // No additional info (key/value pairs)
    321 				for (int level = 0; level < nLevels; level++) {
    322 					int faceLodSize = images[0][level].getSize();
    323 					int faceLodSizeRounded = (faceLodSize + 3) & ~3;
    324 					out.writeInt(faceLodSize);
    325 					for (int face = 0; face < nFaces; face++) {
    326 						byte[] bytes = images[face][level].getBytes();
    327 						out.write(bytes);
    328 						for (int j = bytes.length; j < faceLodSizeRounded; j++)
    329 							out.write((byte)0x00);
    330 					}
    331 				}
    332 
    333 				out.close();
    334 			} catch (Exception e) {
    335 				Gdx.app.error("KTXProcessor", "Error writing to file: " + output.getName(), e);
    336 			}
    337 		}
    338 	}
    339 
    340 	private static class Image {
    341 
    342 		public ETC1Data etcData;
    343 		public Pixmap pixmap;
    344 
    345 		public Image () {
    346 		}
    347 
    348 		public int getSize () {
    349 			if (etcData != null) return etcData.compressedData.limit() - etcData.dataOffset;
    350 			throw new GdxRuntimeException("Unsupported output format, try adding '-etc1' as argument");
    351 		}
    352 
    353 		public byte[] getBytes () {
    354 			if (etcData != null) {
    355 				byte[] result = new byte[getSize()];
    356 				etcData.compressedData.position(etcData.dataOffset);
    357 				etcData.compressedData.get(result);
    358 				return result;
    359 			}
    360 			throw new GdxRuntimeException("Unsupported output format, try adding '-etc1' as argument");
    361 		}
    362 
    363 	}
    364 }
    365