
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