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