|
@@ -0,0 +1,354 @@
|
|
|
|
+/*
|
|
|
|
+ * Copyright (c) 2009-2015 jMonkeyEngine
|
|
|
|
+ * All rights reserved.
|
|
|
|
+ *
|
|
|
|
+ * Redistribution and use in source and binary forms, with or without
|
|
|
|
+ * modification, are permitted provided that the following conditions are
|
|
|
|
+ * met:
|
|
|
|
+ *
|
|
|
|
+ * * Redistributions of source code must retain the above copyright
|
|
|
|
+ * notice, this list of conditions and the following disclaimer.
|
|
|
|
+ *
|
|
|
|
+ * * Redistributions in binary form must reproduce the above copyright
|
|
|
|
+ * notice, this list of conditions and the following disclaimer in the
|
|
|
|
+ * documentation and/or other materials provided with the distribution.
|
|
|
|
+ *
|
|
|
|
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
|
|
|
|
+ * may be used to endorse or promote products derived from this software
|
|
|
|
+ * without specific prior written permission.
|
|
|
|
+ *
|
|
|
|
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
|
|
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
|
|
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
|
|
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
|
|
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
|
|
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
|
|
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
|
|
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
|
|
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
|
|
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
|
|
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
+ */
|
|
|
|
+package com.jme3.texture.plugins.ktx;
|
|
|
|
+
|
|
|
|
+import com.jme3.asset.AssetInfo;
|
|
|
|
+import com.jme3.asset.AssetLoader;
|
|
|
|
+import com.jme3.asset.TextureKey;
|
|
|
|
+import com.jme3.renderer.Caps;
|
|
|
|
+import com.jme3.renderer.opengl.GLImageFormat;
|
|
|
|
+import com.jme3.renderer.opengl.GLImageFormats;
|
|
|
|
+import com.jme3.texture.Image;
|
|
|
|
+import com.jme3.texture.image.ColorSpace;
|
|
|
|
+import com.jme3.util.BufferUtils;
|
|
|
|
+import com.jme3.util.LittleEndien;
|
|
|
|
+import java.io.DataInput;
|
|
|
|
+import java.io.DataInputStream;
|
|
|
|
+import java.io.IOException;
|
|
|
|
+import java.io.InputStream;
|
|
|
|
+import java.nio.ByteBuffer;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.Collections;
|
|
|
|
+import java.util.EnumSet;
|
|
|
|
+import java.util.logging.Level;
|
|
|
|
+import java.util.logging.Logger;
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ *
|
|
|
|
+ * A KTX file loader
|
|
|
|
+ * KTX file format is an image container defined by the Kronos group
|
|
|
|
+ * See specs here https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/
|
|
|
|
+ *
|
|
|
|
+ * This loader doesn't support compressed files yet.
|
|
|
|
+ *
|
|
|
|
+ * @author Nehon
|
|
|
|
+ */
|
|
|
|
+public class KTXLoader implements AssetLoader {
|
|
|
|
+
|
|
|
|
+ private final static Logger log = Logger.getLogger(KTXLoader.class.getName());
|
|
|
|
+
|
|
|
|
+ private final static byte[] fileIdentifier = {
|
|
|
|
+ (byte) 0xAB, (byte) 0x4B, (byte) 0x54, (byte) 0x58, (byte) 0x20, (byte) 0x31, (byte) 0x31, (byte) 0xBB, (byte) 0x0D, (byte) 0x0A, (byte) 0x1A, (byte) 0x0A
|
|
|
|
+ };
|
|
|
|
+ private boolean slicesInside = false;
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public Object load(AssetInfo info) throws IOException {
|
|
|
|
+ if (!(info.getKey() instanceof TextureKey)) {
|
|
|
|
+ throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ InputStream in = null;
|
|
|
|
+ try {
|
|
|
|
+ in = info.openStream();
|
|
|
|
+ Image img = load(in);
|
|
|
|
+ return img;
|
|
|
|
+ } finally {
|
|
|
|
+ if (in != null) {
|
|
|
|
+ in.close();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private Image load(InputStream stream) {
|
|
|
|
+
|
|
|
|
+ byte[] fileId = new byte[12];
|
|
|
|
+
|
|
|
|
+ DataInput in = new DataInputStream(stream);
|
|
|
|
+ try {
|
|
|
|
+ stream.read(fileId, 0, 12);
|
|
|
|
+ if (!checkFileIdentifier(fileId)) {
|
|
|
|
+ throw new IllegalArgumentException("Unrecognized ktx file identifier : " + new String(fileId) + " should be " + new String(fileIdentifier));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ int endianness = in.readInt();
|
|
|
|
+ //opposite endianness
|
|
|
|
+ if (endianness == 0x01020304) {
|
|
|
|
+ in = new LittleEndien(stream);
|
|
|
|
+ }
|
|
|
|
+ int glType = in.readInt();
|
|
|
|
+ int glTypeSize = in.readInt();
|
|
|
|
+ int glFormat = in.readInt();
|
|
|
|
+ int glInternalFormat = in.readInt();
|
|
|
|
+ int glBaseInternalFormat = in.readInt();
|
|
|
|
+ int pixelWidth = in.readInt();
|
|
|
|
+ int pixelHeight = in.readInt();
|
|
|
|
+ int pixelDepth = in.readInt();
|
|
|
|
+ int numberOfArrayElements = in.readInt();
|
|
|
|
+ int numberOfFaces = in.readInt();
|
|
|
|
+ int numberOfMipmapLevels = in.readInt();
|
|
|
|
+ int bytesOfKeyValueData = in.readInt();
|
|
|
|
+
|
|
|
|
+ log.log(Level.FINE, "glType = {0}", glType);
|
|
|
|
+ log.log(Level.FINE, "glTypeSize = {0}", glTypeSize);
|
|
|
|
+ log.log(Level.FINE, "glFormat = {0}", glFormat);
|
|
|
|
+ log.log(Level.FINE, "glInternalFormat = {0}", glInternalFormat);
|
|
|
|
+ log.log(Level.FINE, "glBaseInternalFormat = {0}", glBaseInternalFormat);
|
|
|
|
+ log.log(Level.FINE, "pixelWidth = {0}", pixelWidth);
|
|
|
|
+ log.log(Level.FINE, "pixelHeight = {0}", pixelHeight);
|
|
|
|
+ log.log(Level.FINE, "pixelDepth = {0}", pixelDepth);
|
|
|
|
+ log.log(Level.FINE, "numberOfArrayElements = {0}", numberOfArrayElements);
|
|
|
|
+ log.log(Level.FINE, "numberOfFaces = {0}", numberOfFaces);
|
|
|
|
+ log.log(Level.FINE, "numberOfMipmapLevels = {0}", numberOfMipmapLevels);
|
|
|
|
+ log.log(Level.FINE, "bytesOfKeyValueData = {0}", bytesOfKeyValueData);
|
|
|
|
+
|
|
|
|
+ if((numberOfFaces >1 && pixelDepth >1) || (numberOfFaces >1 && numberOfArrayElements >1) || (pixelDepth >1 && numberOfArrayElements >1)){
|
|
|
|
+ throw new UnsupportedOperationException("jME doesn't support cube maps of 3D textures or arrays of 3D texture or arrays of cube map of 3d textures");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ PixelReader pixelReader = parseMetaData(bytesOfKeyValueData, in);
|
|
|
|
+ if (pixelReader == null){
|
|
|
|
+ pixelReader = new SrTuRoPixelReader();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //some of the values may be 0 we need them at least to be 1
|
|
|
|
+ pixelDepth = Math.max(1, pixelDepth);
|
|
|
|
+ numberOfArrayElements = Math.max(1, numberOfArrayElements);
|
|
|
|
+ numberOfFaces = Math.max(1, numberOfFaces);
|
|
|
|
+ numberOfMipmapLevels = Math.max(1, numberOfMipmapLevels);
|
|
|
|
+
|
|
|
|
+ int nbSlices = Math.max(numberOfFaces,numberOfArrayElements);
|
|
|
|
+
|
|
|
|
+ Image.Format imgFormat = getImageFormat(glFormat, glInternalFormat, glType);
|
|
|
|
+ log.log(Level.FINE, "img format {0}", imgFormat.toString());
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ int bytePerPixel = imgFormat.getBitsPerPixel() / 8;
|
|
|
|
+ int byteBuffersSize = computeBuffersSize(numberOfMipmapLevels, pixelWidth, pixelHeight, bytePerPixel, pixelDepth);
|
|
|
|
+ log.log(Level.FINE, "data size {0}", byteBuffersSize);
|
|
|
|
+
|
|
|
|
+ int[] mipMapSizes = new int[numberOfMipmapLevels];
|
|
|
|
+
|
|
|
|
+ Image image = createImage(nbSlices, byteBuffersSize, imgFormat, pixelWidth, pixelHeight, pixelDepth);
|
|
|
|
+
|
|
|
|
+ byte[] pixelData = new byte[bytePerPixel];
|
|
|
|
+
|
|
|
|
+ int offset = 0;
|
|
|
|
+ //iterate over data
|
|
|
|
+ for (int mipLevel = 0; mipLevel < numberOfMipmapLevels; mipLevel++) {
|
|
|
|
+ //size of the image in byte.
|
|
|
|
+ //this value is bogus in many example, when using mipmaps.
|
|
|
|
+ //instead we compute the theorical size and display a warning when it does not match.
|
|
|
|
+ int fileImageSize = in.readInt();
|
|
|
|
+
|
|
|
|
+ int width = Math.max(1, pixelWidth >> mipLevel);
|
|
|
|
+ int height = Math.max(1, pixelHeight >> mipLevel);
|
|
|
|
+
|
|
|
|
+ int imageSize = width * height * bytePerPixel;
|
|
|
|
+ mipMapSizes[mipLevel] = imageSize;
|
|
|
|
+ log.log(Level.FINE, "current mip size {0}", imageSize);
|
|
|
|
+ if(fileImageSize != imageSize){
|
|
|
|
+ log.log(Level.WARNING, "Mip map size is wrong in the file for mip level {0} size is {1} should be {2}", new Object[]{mipLevel, fileImageSize, imageSize});
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ for (int arrayElem = 0; arrayElem < numberOfArrayElements; arrayElem++) {
|
|
|
|
+ for (int face = 0; face < numberOfFaces; face++) {
|
|
|
|
+ int nbPixelRead = 0;
|
|
|
|
+ for (int depth = 0; depth < pixelDepth; depth++) {
|
|
|
|
+ ByteBuffer byteBuffer = image.getData(getSlice(face, arrayElem));
|
|
|
|
+
|
|
|
|
+ log.log(Level.FINE, "position {0}", byteBuffer.position());
|
|
|
|
+ byteBuffer.position(offset);
|
|
|
|
+ nbPixelRead = pixelReader.readPixels(width, height, pixelData, byteBuffer, in);
|
|
|
|
+ }
|
|
|
|
+ //cube padding
|
|
|
|
+ if (numberOfFaces == 6 && numberOfArrayElements == 0) {
|
|
|
|
+ in.skipBytes(3 - ((nbPixelRead + 3) % 4));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ //mip padding
|
|
|
|
+ log.log(Level.FINE, "skipping {0}", (3 - ((imageSize + 3) % 4)));
|
|
|
|
+ in.skipBytes(3 - ((imageSize + 3) % 4));
|
|
|
|
+ offset+=imageSize;
|
|
|
|
+ }
|
|
|
|
+ //there are loaded mip maps we set the sizes
|
|
|
|
+ if(numberOfMipmapLevels >1){
|
|
|
|
+ image.setMipMapSizes(mipMapSizes);
|
|
|
|
+ }
|
|
|
|
+ //if 3D texture and slices' orientation is inside, we reverse the data array.
|
|
|
|
+ if(pixelDepth > 1 && slicesInside){
|
|
|
|
+ Collections.reverse(image.getData());
|
|
|
|
+ }
|
|
|
|
+ return image;
|
|
|
|
+
|
|
|
|
+ } catch (IOException ex) {
|
|
|
|
+ Logger.getLogger(KTXLoader.class.getName()).log(Level.SEVERE, null, ex);
|
|
|
|
+ }
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * returns the slice from the face and the array index
|
|
|
|
+ * @param face the face
|
|
|
|
+ * @param arrayElem the array index
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private static int getSlice(int face, int arrayElem) {
|
|
|
|
+ return Math.max(face, arrayElem);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Computes a buffer size from given parameters
|
|
|
|
+ * @param numberOfMipmapLevels
|
|
|
|
+ * @param pixelWidth
|
|
|
|
+ * @param pixelHeight
|
|
|
|
+ * @param bytePerPixel
|
|
|
|
+ * @param pixelDepth
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private int computeBuffersSize(int numberOfMipmapLevels, int pixelWidth, int pixelHeight, int bytePerPixel, int pixelDepth) {
|
|
|
|
+ int byteBuffersSize = 0;
|
|
|
|
+ for (int mipLevel = 0; mipLevel < numberOfMipmapLevels; mipLevel++) {
|
|
|
|
+ int width = Math.max(1, pixelWidth >> mipLevel);
|
|
|
|
+ int height = Math.max(1, pixelHeight >> mipLevel);
|
|
|
|
+ byteBuffersSize += width * height * bytePerPixel;
|
|
|
|
+ log.log(Level.FINE, "mip level size : {0} : {1}", new Object[]{mipLevel, width * height * bytePerPixel});
|
|
|
|
+ }
|
|
|
|
+ return byteBuffersSize * pixelDepth;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Create an image with given parameters
|
|
|
|
+ * @param nbSlices
|
|
|
|
+ * @param byteBuffersSize
|
|
|
|
+ * @param imgFormat
|
|
|
|
+ * @param pixelWidth
|
|
|
|
+ * @param pixelHeight
|
|
|
|
+ * @param depth
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private Image createImage(int nbSlices, int byteBuffersSize, Image.Format imgFormat, int pixelWidth, int pixelHeight, int depth) {
|
|
|
|
+ ArrayList<ByteBuffer> imageData = new ArrayList<ByteBuffer>(nbSlices);
|
|
|
|
+ for (int i = 0; i < nbSlices; i++) {
|
|
|
|
+ imageData.add(BufferUtils.createByteBuffer(byteBuffersSize));
|
|
|
|
+ }
|
|
|
|
+ Image image = new Image(imgFormat, pixelWidth, pixelHeight, depth, imageData, ColorSpace.sRGB);
|
|
|
|
+ return image;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Parse the file metaData to select the PixelReader that suits the file
|
|
|
|
+ * coordinates orientation
|
|
|
|
+ * @param bytesOfKeyValueData
|
|
|
|
+ * @param in
|
|
|
|
+ * @return
|
|
|
|
+ * @throws IOException
|
|
|
|
+ */
|
|
|
|
+ private PixelReader parseMetaData(int bytesOfKeyValueData, DataInput in) throws IOException {
|
|
|
|
+ PixelReader pixelReader = null;
|
|
|
|
+ for (int i = 0; i < bytesOfKeyValueData;) {
|
|
|
|
+ //reading key values
|
|
|
|
+ int keyAndValueByteSize = in.readInt();
|
|
|
|
+ byte[] keyValue = new byte[keyAndValueByteSize];
|
|
|
|
+ in.readFully(keyValue);
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ //parsing key values
|
|
|
|
+ String[] kv = new String(keyValue).split("\0");
|
|
|
|
+ for (int j = 0; j < kv.length; j += 2) {
|
|
|
|
+ System.err.println("key : " + kv[j]);
|
|
|
|
+ System.err.println("value : " + kv[j + 1]);
|
|
|
|
+ if(kv[j].equalsIgnoreCase("KTXorientation")){
|
|
|
|
+ if(kv[j + 1].startsWith("S=r,T=d") ){
|
|
|
|
+ pixelReader = new SrTdRiPixelReader();
|
|
|
|
+ }else{
|
|
|
|
+ pixelReader = new SrTuRoPixelReader();
|
|
|
|
+ }
|
|
|
|
+ if(kv[j + 1].contains("R=i")){
|
|
|
|
+ slicesInside = true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //padding
|
|
|
|
+ int padding = 3 - ((keyAndValueByteSize + 3) % 4);
|
|
|
|
+ if (padding > 0) {
|
|
|
|
+ in.skipBytes(padding);
|
|
|
|
+ }
|
|
|
|
+ i += 4 + keyAndValueByteSize + padding;
|
|
|
|
+ }
|
|
|
|
+ return pixelReader;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Chacks the file id
|
|
|
|
+ * @param b
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private boolean checkFileIdentifier(byte[] b) {
|
|
|
|
+ boolean check = true;
|
|
|
|
+ for (int i = 0; i < 12; i++) {
|
|
|
|
+ if (b[i] != fileIdentifier[i]) {
|
|
|
|
+ check = false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return check;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * returns the JME image format from gl formats and types.
|
|
|
|
+ * @param glFormat
|
|
|
|
+ * @param glInternalFormat
|
|
|
|
+ * @param glType
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private Image.Format getImageFormat(int glFormat, int glInternalFormat, int glType) {
|
|
|
|
+ EnumSet<Caps> caps = EnumSet.allOf(Caps.class);
|
|
|
|
+ GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps);
|
|
|
|
+ for (GLImageFormat[] format : formats) {
|
|
|
|
+ for (int j = 0; j < format.length; j++) {
|
|
|
|
+ GLImageFormat glImgFormat = format[j];
|
|
|
|
+ if (glImgFormat != null) {
|
|
|
|
+ if (glImgFormat.format == glFormat && glImgFormat.dataType == glType) {
|
|
|
|
+ if (glFormat == glInternalFormat || glImgFormat.internalFormat == glInternalFormat) {
|
|
|
|
+ return Image.Format.values()[j];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|