diff options
author | Scott Barta <sbarta@google.com> | 2012-03-01 12:35:35 -0800 |
---|---|---|
committer | Scott Barta <sbarta@google.com> | 2012-03-01 12:40:08 -0800 |
commit | 59b2e6871c65f58fdad78cd7229c292f6a177578 (patch) | |
tree | 2d4e7bfc05b93f40b34675d77e403dd1c25efafd /engine/src/core-plugins/com/jme3 | |
parent | f9b30489e75ac1eabc365064959804e99534f7ab (diff) | |
download | jmonkeyengine-59b2e6871c65f58fdad78cd7229c292f6a177578.tar.gz |
Adds the jMonkeyEngine library to the build.
Adds the jMonkeyEngine open source 3D game engine to the build. This
is built as a static library and is only used by the Finsky client.
Change-Id: I06a3f054df7b8a67757267d884854f70c5a16ca0
Diffstat (limited to 'engine/src/core-plugins/com/jme3')
27 files changed, 9214 insertions, 0 deletions
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java new file mode 100644 index 0000000..f3570de --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2009-2010 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.asset.plugins; + +import com.jme3.asset.*; +import com.jme3.system.JmeSystem; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.logging.Logger; + +/** + * The <code>ClasspathLocator</code> looks up an asset in the classpath. + * @author Kirill Vainer + */ +public class ClasspathLocator implements AssetLocator { + + private static final Logger logger = Logger.getLogger(ClasspathLocator.class.getName()); + private String root = ""; + + public ClasspathLocator(){ + } + + public void setRootPath(String rootPath) { + this.root = rootPath; + if (root.equals("/")) + root = ""; + else if (root.length() > 1){ + if (root.startsWith("/")){ + root = root.substring(1); + } + if (!root.endsWith("/")) + root += "/"; + } + } + + public AssetInfo locate(AssetManager manager, AssetKey key) { + URL url; + String name = key.getName(); + if (name.startsWith("/")) + name = name.substring(1); + + name = root + name; +// if (!name.startsWith(root)){ +// name = root + name; +// } + + if (JmeSystem.isLowPermissions()){ + url = ClasspathLocator.class.getResource("/" + name); + }else{ + url = Thread.currentThread().getContextClassLoader().getResource(name); + } + if (url == null) + return null; + + if (url.getProtocol().equals("file")){ + try { + String path = new File(url.toURI()).getCanonicalPath(); + + // convert to / for windows + if (File.separatorChar == '\\'){ + path = path.replace('\\', '/'); + } + + // compare path + if (!path.endsWith(name)){ + throw new AssetNotFoundException("Asset name doesn't match requirements.\n"+ + "\"" + path + "\" doesn't match \"" + name + "\""); + } + } catch (URISyntaxException ex) { + throw new AssetLoadException("Error converting URL to URI", ex); + } catch (IOException ex){ + throw new AssetLoadException("Failed to get canonical path for " + url, ex); + } + } + + try{ + return UrlAssetInfo.create(manager, key, url); + }catch (IOException ex){ + // This is different handling than URL locator + // since classpath locating would return null at the getResource() + // call, otherwise there's a more critical error... + throw new AssetLoadException("Failed to read URL " + url, ex); + } + } +} diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java new file mode 100644 index 0000000..f448fa3 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2009-2010 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.asset.plugins; + +import com.jme3.asset.*; +import java.io.*; + +/** + * <code>FileLocator</code> allows you to specify a folder where to + * look for assets. + * @author Kirill Vainer + */ +public class FileLocator implements AssetLocator { + + private File root; + + public void setRootPath(String rootPath) { + if (rootPath == null) + throw new NullPointerException(); + + try { + root = new File(rootPath).getCanonicalFile(); + if (!root.isDirectory()){ + throw new IllegalArgumentException("Given root path \"" + root + "\" not a directory"); + } + } catch (IOException ex) { + throw new AssetLoadException("Root path is invalid", ex); + } + } + + private static class AssetInfoFile extends AssetInfo { + + private File file; + + public AssetInfoFile(AssetManager manager, AssetKey key, File file){ + super(manager, key); + this.file = file; + } + + @Override + public InputStream openStream() { + try{ + return new FileInputStream(file); + }catch (FileNotFoundException ex){ + // NOTE: Can still happen even if file.exists() is true, e.g. + // permissions issue and similar + throw new AssetLoadException("Failed to open file: " + file, ex); + } + } + } + + public AssetInfo locate(AssetManager manager, AssetKey key) { + String name = key.getName(); + File file = new File(root, name); + if (file.exists() && file.isFile()){ + try { + // Now, check asset name requirements + String canonical = file.getCanonicalPath(); + String absolute = file.getAbsolutePath(); + if (!canonical.endsWith(absolute)){ + throw new AssetNotFoundException("Asset name doesn't match requirements.\n"+ + "\"" + canonical + "\" doesn't match \"" + absolute + "\""); + } + } catch (IOException ex) { + throw new AssetLoadException("Failed to get file canonical path " + file, ex); + } + + return new AssetInfoFile(manager, key, file); + }else{ + return null; + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java new file mode 100644 index 0000000..0637221 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2009-2010 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.asset.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLocator; +import com.jme3.asset.AssetManager; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; + +public class HttpZipLocator implements AssetLocator { + + private static final Logger logger = Logger.getLogger(HttpZipLocator.class.getName()); + + private URL zipUrl; + private String rootPath = ""; + private int numEntries; + private int tableOffset; + private int tableLength; + private HashMap<String, ZipEntry2> entries; + + private static final ByteBuffer byteBuf = ByteBuffer.allocate(250); + private static final CharBuffer charBuf = CharBuffer.allocate(250); + private static final CharsetDecoder utf8Decoder; + + public static final long LOCSIG = 0x4034b50, EXTSIG = 0x8074b50, + CENSIG = 0x2014b50, ENDSIG = 0x6054b50; + + public static final int LOCHDR = 30, EXTHDR = 16, CENHDR = 46, ENDHDR = 22, + LOCVER = 4, LOCFLG = 6, LOCHOW = 8, LOCTIM = 10, LOCCRC = 14, + LOCSIZ = 18, LOCLEN = 22, LOCNAM = 26, LOCEXT = 28, EXTCRC = 4, + EXTSIZ = 8, EXTLEN = 12, CENVEM = 4, CENVER = 6, CENFLG = 8, + CENHOW = 10, CENTIM = 12, CENCRC = 16, CENSIZ = 20, CENLEN = 24, + CENNAM = 28, CENEXT = 30, CENCOM = 32, CENDSK = 34, CENATT = 36, + CENATX = 38, CENOFF = 42, ENDSUB = 8, ENDTOT = 10, ENDSIZ = 12, + ENDOFF = 16, ENDCOM = 20; + + static { + Charset utf8 = Charset.forName("UTF-8"); + utf8Decoder = utf8.newDecoder(); + } + + private static class ZipEntry2 { + String name; + int length; + int offset; + int compSize; + long crc; + boolean deflate; + + @Override + public String toString(){ + return "ZipEntry[name=" + name + + ", length=" + length + + ", compSize=" + compSize + + ", offset=" + offset + "]"; + } + } + + private static int get16(byte[] b, int off) { + return (b[off++] & 0xff) | + ((b[off] & 0xff) << 8); + } + + private static int get32(byte[] b, int off) { + return (b[off++] & 0xff) | + ((b[off++] & 0xff) << 8) | + ((b[off++] & 0xff) << 16) | + ((b[off] & 0xff) << 24); + } + + private static long getu32(byte[] b, int off) throws IOException{ + return (b[off++]&0xff) | + ((b[off++]&0xff) << 8) | + ((b[off++]&0xff) << 16) | + (((long)(b[off]&0xff)) << 24); + } + + private static String getUTF8String(byte[] b, int off, int len) throws CharacterCodingException { + StringBuilder sb = new StringBuilder(); + + int read = 0; + while (read < len){ + // Either read n remaining bytes in b or 250 if n is higher. + int toRead = Math.min(len - read, byteBuf.capacity()); + + boolean endOfInput = toRead < byteBuf.capacity(); + + // read 'toRead' bytes into byteBuf + byteBuf.put(b, off + read, toRead); + + // set limit to position and set position to 0 + // so data can be decoded + byteBuf.flip(); + + // decode data in byteBuf + CoderResult result = utf8Decoder.decode(byteBuf, charBuf, endOfInput); + + // if the result is not an underflow its an error + // that cannot be handled. + // if the error is an underflow and its the end of input + // then the decoder expects more bytes but there are no more => error + if (!result.isUnderflow() || !endOfInput){ + result.throwException(); + } + + // flip the char buf to get the string just decoded + charBuf.flip(); + + // append the decoded data into the StringBuilder + sb.append(charBuf.toString()); + + // clear buffers for next use + byteBuf.clear(); + charBuf.clear(); + + read += toRead; + } + + return sb.toString(); + } + + private InputStream readData(int offset, int length) throws IOException{ + HttpURLConnection conn = (HttpURLConnection) zipUrl.openConnection(); + conn.setDoOutput(false); + conn.setUseCaches(false); + conn.setInstanceFollowRedirects(false); + String range = "-"; + if (offset != Integer.MAX_VALUE){ + range = offset + range; + } + if (length != Integer.MAX_VALUE){ + if (offset != Integer.MAX_VALUE){ + range = range + (offset + length - 1); + }else{ + range = range + length; + } + } + + conn.setRequestProperty("Range", "bytes=" + range); + conn.connect(); + if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){ + return conn.getInputStream(); + }else if (conn.getResponseCode() == HttpURLConnection.HTTP_OK){ + throw new IOException("Your server does not support HTTP feature Content-Range. Please contact your server administrator."); + }else{ + throw new IOException(conn.getResponseCode() + " " + conn.getResponseMessage()); + } + } + + private int readTableEntry(byte[] table, int offset) throws IOException{ + if (get32(table, offset) != CENSIG){ + throw new IOException("Central directory error, expected 'PK12'"); + } + + int nameLen = get16(table, offset + CENNAM); + int extraLen = get16(table, offset + CENEXT); + int commentLen = get16(table, offset + CENCOM); + int newOffset = offset + CENHDR + nameLen + extraLen + commentLen; + + int flags = get16(table, offset + CENFLG); + if ((flags & 1) == 1){ + // ignore this entry, it uses encryption + return newOffset; + } + + int method = get16(table, offset + CENHOW); + if (method != ZipEntry.DEFLATED && method != ZipEntry.STORED){ + // ignore this entry, it uses unknown compression method + return newOffset; + } + + String name = getUTF8String(table, offset + CENHDR, nameLen); + if (name.charAt(name.length()-1) == '/'){ + // ignore this entry, it is directory node + // or it has no name (?) + return newOffset; + } + + ZipEntry2 entry = new ZipEntry2(); + entry.name = name; + entry.deflate = (method == ZipEntry.DEFLATED); + entry.crc = getu32(table, offset + CENCRC); + entry.length = get32(table, offset + CENLEN); + entry.compSize = get32(table, offset + CENSIZ); + entry.offset = get32(table, offset + CENOFF); + + // we want offset directly into file data .. + // move the offset forward to skip the LOC header + entry.offset += LOCHDR + nameLen + extraLen; + + entries.put(entry.name, entry); + + return newOffset; + } + + private void fillByteArray(byte[] array, InputStream source) throws IOException{ + int total = 0; + int length = array.length; + while (total < length) { + int read = source.read(array, total, length - total); + if (read < 0) + throw new IOException("Failed to read entire array"); + + total += read; + } + } + + private void readCentralDirectory() throws IOException{ + InputStream in = readData(tableOffset, tableLength); + byte[] header = new byte[tableLength]; + + // Fix for "PK12 bug in town.zip": sometimes + // not entire byte array will be read with InputStream.read() + // (especially for big headers) + fillByteArray(header, in); + +// in.read(header); + in.close(); + + entries = new HashMap<String, ZipEntry2>(numEntries); + int offset = 0; + for (int i = 0; i < numEntries; i++){ + offset = readTableEntry(header, offset); + } + } + + private void readEndHeader() throws IOException{ + +// InputStream in = readData(Integer.MAX_VALUE, ENDHDR); +// byte[] header = new byte[ENDHDR]; +// fillByteArray(header, in); +// in.close(); +// +// if (get32(header, 0) != ENDSIG){ +// throw new IOException("End header error, expected 'PK56'"); +// } + + // Fix for "PK56 bug in town.zip": + // If there's a zip comment inside the end header, + // PK56 won't appear in the -22 position relative to the end of the + // file! + // In that case, we have to search for it. + // Increase search space to 200 bytes + + InputStream in = readData(Integer.MAX_VALUE, 200); + byte[] header = new byte[200]; + fillByteArray(header, in); + in.close(); + + int offset = -1; + for (int i = 200 - 22; i >= 0; i--){ + if (header[i] == (byte) (ENDSIG & 0xff) + && get32(header, i) == ENDSIG){ + // found location + offset = i; + break; + } + } + if (offset == -1) + throw new IOException("Cannot find Zip End Header in file!"); + + numEntries = get16(header, offset + ENDTOT); + tableLength = get32(header, offset + ENDSIZ); + tableOffset = get32(header, offset + ENDOFF); + } + + public void load(URL url) throws IOException { + if (!url.getProtocol().equals("http")) + throw new UnsupportedOperationException(); + + zipUrl = url; + readEndHeader(); + readCentralDirectory(); + } + + private InputStream openStream(ZipEntry2 entry) throws IOException{ + InputStream in = readData(entry.offset, entry.compSize); + if (entry.deflate){ + return new InflaterInputStream(in, new Inflater(true)); + } + return in; + } + + public InputStream openStream(String name) throws IOException{ + ZipEntry2 entry = entries.get(name); + if (entry == null) + throw new RuntimeException("Entry not found: "+name); + + return openStream(entry); + } + + public void setRootPath(String path){ + if (!rootPath.equals(path)){ + rootPath = path; + try { + load(new URL(path)); + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed to set root path "+path, ex); + } + } + } + + public AssetInfo locate(AssetManager manager, AssetKey key){ + final ZipEntry2 entry = entries.get(key.getName()); + if (entry == null) + return null; + + return new AssetInfo(manager, key){ + @Override + public InputStream openStream() { + try { + return HttpZipLocator.this.openStream(entry); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error retrieving "+entry.name, ex); + return null; + } + } + }; + } + +} diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~ b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~ new file mode 100644 index 0000000..f5fbfd1 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~ @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2009-2010 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.asset.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLocator; +import com.jme3.asset.AssetManager; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; + +public class HttpZipLocator implements AssetLocator { + + private static final Logger logger = Logger.getLogger(HttpZipLocator.class.getName()); + + private URL zipUrl; + private String rootPath = ""; + private int numEntries; + private int tableOffset; + private int tableLength; + private HashMap<String, ZipEntry2> entries; + + private static final ByteBuffer byteBuf = ByteBuffer.allocate(250); + private static final CharBuffer charBuf = CharBuffer.allocate(250); + private static final CharsetDecoder utf8Decoder; + + static { + Charset utf8 = Charset.forName("UTF-8"); + utf8Decoder = utf8.newDecoder(); + } + + private static class ZipEntry2 { + String name; + int length; + int offset; + int compSize; + long crc; + boolean deflate; + + @Override + public String toString(){ + return "ZipEntry[name=" + name + + ", length=" + length + + ", compSize=" + compSize + + ", offset=" + offset + "]"; + } + } + + private static int get16(byte[] b, int off) { + return (b[off++] & 0xff) | + ((b[off] & 0xff) << 8); + } + + private static int get32(byte[] b, int off) { + return (b[off++] & 0xff) | + ((b[off++] & 0xff) << 8) | + ((b[off++] & 0xff) << 16) | + ((b[off] & 0xff) << 24); + } + + private static long getu32(byte[] b, int off) throws IOException{ + return (b[off++]&0xff) | + ((b[off++]&0xff) << 8) | + ((b[off++]&0xff) << 16) | + (((long)(b[off]&0xff)) << 24); + } + + private static String getUTF8String(byte[] b, int off, int len) throws CharacterCodingException { + StringBuilder sb = new StringBuilder(); + + int read = 0; + while (read < len){ + // Either read n remaining bytes in b or 250 if n is higher. + int toRead = Math.min(len - read, byteBuf.capacity()); + + boolean endOfInput = toRead < byteBuf.capacity(); + + // read 'toRead' bytes into byteBuf + byteBuf.put(b, off + read, toRead); + + // set limit to position and set position to 0 + // so data can be decoded + byteBuf.flip(); + + // decode data in byteBuf + CoderResult result = utf8Decoder.decode(byteBuf, charBuf, endOfInput); + + // if the result is not an underflow its an error + // that cannot be handled. + // if the error is an underflow and its the end of input + // then the decoder expects more bytes but there are no more => error + if (!result.isUnderflow() || !endOfInput){ + result.throwException(); + } + + // flip the char buf to get the string just decoded + charBuf.flip(); + + // append the decoded data into the StringBuilder + sb.append(charBuf.toString()); + + // clear buffers for next use + byteBuf.clear(); + charBuf.clear(); + + read += toRead; + } + + return sb.toString(); + } + + private InputStream readData(int offset, int length) throws IOException{ + HttpURLConnection conn = (HttpURLConnection) zipUrl.openConnection(); + conn.setDoOutput(false); + conn.setUseCaches(false); + conn.setInstanceFollowRedirects(false); + String range = "-"; + if (offset != Integer.MAX_VALUE){ + range = offset + range; + } + if (length != Integer.MAX_VALUE){ + if (offset != Integer.MAX_VALUE){ + range = range + (offset + length - 1); + }else{ + range = range + length; + } + } + + conn.setRequestProperty("Range", "bytes=" + range); + conn.connect(); + if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){ + return conn.getInputStream(); + }else if (conn.getResponseCode() == HttpURLConnection.HTTP_OK){ + throw new IOException("Your server does not support HTTP feature Content-Range. Please contact your server administrator."); + }else{ + throw new IOException(conn.getResponseCode() + " " + conn.getResponseMessage()); + } + } + + private int readTableEntry(byte[] table, int offset) throws IOException{ + if (get32(table, offset) != ZipEntry.CENSIG){ + throw new IOException("Central directory error, expected 'PK12'"); + } + + int nameLen = get16(table, offset + ZipEntry.CENNAM); + int extraLen = get16(table, offset + ZipEntry.CENEXT); + int commentLen = get16(table, offset + ZipEntry.CENCOM); + int newOffset = offset + ZipEntry.CENHDR + nameLen + extraLen + commentLen; + + int flags = get16(table, offset + ZipEntry.CENFLG); + if ((flags & 1) == 1){ + // ignore this entry, it uses encryption + return newOffset; + } + + int method = get16(table, offset + ZipEntry.CENHOW); + if (method != ZipEntry.DEFLATED && method != ZipEntry.STORED){ + // ignore this entry, it uses unknown compression method + return newOffset; + } + + String name = getUTF8String(table, offset + ZipEntry.CENHDR, nameLen); + if (name.charAt(name.length()-1) == '/'){ + // ignore this entry, it is directory node + // or it has no name (?) + return newOffset; + } + + ZipEntry2 entry = new ZipEntry2(); + entry.name = name; + entry.deflate = (method == ZipEntry.DEFLATED); + entry.crc = getu32(table, offset + ZipEntry.CENCRC); + entry.length = get32(table, offset + ZipEntry.CENLEN); + entry.compSize = get32(table, offset + ZipEntry.CENSIZ); + entry.offset = get32(table, offset + ZipEntry.CENOFF); + + // we want offset directly into file data .. + // move the offset forward to skip the LOC header + entry.offset += ZipEntry.LOCHDR + nameLen + extraLen; + + entries.put(entry.name, entry); + + return newOffset; + } + + private void fillByteArray(byte[] array, InputStream source) throws IOException{ + int total = 0; + int length = array.length; + while (total < length) { + int read = source.read(array, total, length - total); + if (read < 0) + throw new IOException("Failed to read entire array"); + + total += read; + } + } + + private void readCentralDirectory() throws IOException{ + InputStream in = readData(tableOffset, tableLength); + byte[] header = new byte[tableLength]; + + // Fix for "PK12 bug in town.zip": sometimes + // not entire byte array will be read with InputStream.read() + // (especially for big headers) + fillByteArray(header, in); + +// in.read(header); + in.close(); + + entries = new HashMap<String, ZipEntry2>(numEntries); + int offset = 0; + for (int i = 0; i < numEntries; i++){ + offset = readTableEntry(header, offset); + } + } + + private void readEndHeader() throws IOException{ + +// InputStream in = readData(Integer.MAX_VALUE, ZipEntry.ENDHDR); +// byte[] header = new byte[ZipEntry.ENDHDR]; +// fillByteArray(header, in); +// in.close(); +// +// if (get32(header, 0) != ZipEntry.ENDSIG){ +// throw new IOException("End header error, expected 'PK56'"); +// } + + // Fix for "PK56 bug in town.zip": + // If there's a zip comment inside the end header, + // PK56 won't appear in the -22 position relative to the end of the + // file! + // In that case, we have to search for it. + // Increase search space to 200 bytes + + InputStream in = readData(Integer.MAX_VALUE, 200); + byte[] header = new byte[200]; + fillByteArray(header, in); + in.close(); + + int offset = -1; + for (int i = 200 - 22; i >= 0; i--){ + if (header[i] == (byte) (ZipEntry.ENDSIG & 0xff) + && get32(header, i) == ZipEntry.ENDSIG){ + // found location + offset = i; + break; + } + } + if (offset == -1) + throw new IOException("Cannot find Zip End Header in file!"); + + numEntries = get16(header, offset + ZipEntry.ENDTOT); + tableLength = get32(header, offset + ZipEntry.ENDSIZ); + tableOffset = get32(header, offset + ZipEntry.ENDOFF); + } + + public void load(URL url) throws IOException { + if (!url.getProtocol().equals("http")) + throw new UnsupportedOperationException(); + + zipUrl = url; + readEndHeader(); + readCentralDirectory(); + } + + private InputStream openStream(ZipEntry2 entry) throws IOException{ + InputStream in = readData(entry.offset, entry.compSize); + if (entry.deflate){ + return new InflaterInputStream(in, new Inflater(true)); + } + return in; + } + + public InputStream openStream(String name) throws IOException{ + ZipEntry2 entry = entries.get(name); + if (entry == null) + throw new RuntimeException("Entry not found: "+name); + + return openStream(entry); + } + + public void setRootPath(String path){ + if (!rootPath.equals(path)){ + rootPath = path; + try { + load(new URL(path)); + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed to set root path "+path, ex); + } + } + } + + public AssetInfo locate(AssetManager manager, AssetKey key){ + final ZipEntry2 entry = entries.get(key.getName()); + if (entry == null) + return null; + + return new AssetInfo(manager, key){ + @Override + public InputStream openStream() { + try { + return HttpZipLocator.this.openStream(entry); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error retrieving "+entry.name, ex); + return null; + } + } + }; + } + +} diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java b/engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java new file mode 100644 index 0000000..941764f --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java @@ -0,0 +1,65 @@ +package com.jme3.asset.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLoadException; +import com.jme3.asset.AssetManager; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +/** + * Handles loading of assets from a URL + * + * @author Kirill Vainer + */ +public class UrlAssetInfo extends AssetInfo { + + private URL url; + private InputStream in; + + public static UrlAssetInfo create(AssetManager assetManager, AssetKey key, URL url) throws IOException { + // Check if URL can be reached. This will throw + // IOException which calling code will handle. + URLConnection conn = url.openConnection(); + conn.setUseCaches(false); + InputStream in = conn.getInputStream(); + + // For some reason url cannot be reached? + if (in == null){ + return null; + }else{ + return new UrlAssetInfo(assetManager, key, url, in); + } + } + + private UrlAssetInfo(AssetManager assetManager, AssetKey key, URL url, InputStream in) throws IOException { + super(assetManager, key); + this.url = url; + this.in = in; + } + + public boolean hasInitialConnection(){ + return in != null; + } + + @Override + public InputStream openStream() { + if (in != null){ + // Reuse the already existing stream (only once) + InputStream in2 = in; + in = null; + return in2; + }else{ + // Create a new stream for subsequent invocations. + try { + URLConnection conn = url.openConnection(); + conn.setUseCaches(false); + return conn.getInputStream(); + } catch (IOException ex) { + throw new AssetLoadException("Failed to read URL " + url, ex); + } + } + } +} diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java new file mode 100644 index 0000000..e770930 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2009-2010 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.asset.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLocator; +import com.jme3.asset.AssetManager; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * <code>UrlLocator</code> is a locator that combines a root URL + * and the given path in the AssetKey to construct a new URL + * that allows locating the asset. + * @author Kirill Vainer + */ +public class UrlLocator implements AssetLocator { + + private static final Logger logger = Logger.getLogger(UrlLocator.class.getName()); + private URL root; + + public void setRootPath(String rootPath) { + try { + this.root = new URL(rootPath); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException("Invalid rootUrl specified", ex); + } + } + + public AssetInfo locate(AssetManager manager, AssetKey key) { + String name = key.getName(); + try{ + //TODO: remove workaround for SDK +// URL url = new URL(root, name); + if(name.startsWith("/")){ + name = name.substring(1); + } + URL url = new URL(root.toExternalForm() + name); + return UrlAssetInfo.create(manager, key, url); + }catch (FileNotFoundException e){ + return null; + }catch (IOException ex){ + logger.log(Level.WARNING, "Error while locating " + name, ex); + return null; + } + } + + +} diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java new file mode 100644 index 0000000..ddfdfe8 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009-2010 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.asset.plugins; + +import com.jme3.asset.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * <code>ZipLocator</code> is a locator that looks up resources in a .ZIP file. + * @author Kirill Vainer + */ +public class ZipLocator implements AssetLocator { + + private ZipFile zipfile; + private static final Logger logger = Logger.getLogger(ZipLocator.class.getName()); + + private class JarAssetInfo extends AssetInfo { + + private final ZipEntry entry; + + public JarAssetInfo(AssetManager manager, AssetKey key, ZipEntry entry){ + super(manager, key); + this.entry = entry; + } + + public InputStream openStream(){ + try{ + return zipfile.getInputStream(entry); + }catch (IOException ex){ + throw new AssetLoadException("Failed to load zip entry: "+entry, ex); + } + } + } + + public void setRootPath(String rootPath) { + try{ + zipfile = new ZipFile(new File(rootPath), ZipFile.OPEN_READ); + }catch (IOException ex){ + throw new AssetLoadException("Failed to open zip file: " + rootPath, ex); + } + } + + public AssetInfo locate(AssetManager manager, AssetKey key) { + String name = key.getName(); + ZipEntry entry = zipfile.getEntry(name); + if (entry == null) + return null; + + return new JarAssetInfo(manager, key, entry); + } + +} diff --git a/engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java b/engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java new file mode 100644 index 0000000..ab6a19b --- /dev/null +++ b/engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2009-2010 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.audio.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetLoader; +import com.jme3.audio.AudioBuffer; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioKey; +import com.jme3.audio.AudioStream; +import com.jme3.util.BufferUtils; +import com.jme3.util.LittleEndien; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class WAVLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(WAVLoader.class.getName()); + + // all these are in big endian + private static final int i_RIFF = 0x46464952; + private static final int i_WAVE = 0x45564157; + private static final int i_fmt = 0x20746D66; + private static final int i_data = 0x61746164; + + private boolean readStream = false; + + private AudioBuffer audioBuffer; + private AudioStream audioStream; + private AudioData audioData; + private int bytesPerSec; + private float duration; + + private LittleEndien in; + + private void readFormatChunk(int size) throws IOException{ + // if other compressions are supported, size doesn't have to be 16 +// if (size != 16) +// logger.warning("Expected size of format chunk to be 16"); + + int compression = in.readShort(); + if (compression != 1){ + throw new IOException("WAV Loader only supports PCM wave files"); + } + + int channels = in.readShort(); + int sampleRate = in.readInt(); + + bytesPerSec = in.readInt(); // used to calculate duration + + int bytesPerSample = in.readShort(); + int bitsPerSample = in.readShort(); + + int expectedBytesPerSec = (bitsPerSample * channels * sampleRate) / 8; + if (expectedBytesPerSec != bytesPerSec){ + logger.log(Level.WARNING, "Expected {0} bytes per second, got {1}", + new Object[]{expectedBytesPerSec, bytesPerSec}); + } + + if (bitsPerSample != 8 && bitsPerSample != 16) + throw new IOException("Only 8 and 16 bits per sample are supported!"); + + if ( (bitsPerSample / 8) * channels != bytesPerSample) + throw new IOException("Invalid bytes per sample value"); + + if (bytesPerSample * sampleRate != bytesPerSec) + throw new IOException("Invalid bytes per second value"); + + audioData.setupFormat(channels, bitsPerSample, sampleRate); + + int remaining = size - 16; + if (remaining > 0){ + in.skipBytes(remaining); + } + } + + private void readDataChunkForBuffer(int len) throws IOException { + ByteBuffer data = BufferUtils.createByteBuffer(len); + byte[] buf = new byte[512]; + int read = 0; + while ( (read = in.read(buf)) > 0){ + data.put(buf, 0, Math.min(read, data.remaining()) ); + } + data.flip(); + audioBuffer.updateData(data); + in.close(); + } + + private void readDataChunkForStream(int len) throws IOException { + audioStream.updateData(in, duration); + } + + private AudioData load(InputStream inputStream, boolean stream) throws IOException{ + this.in = new LittleEndien(inputStream); + + int sig = in.readInt(); + if (sig != i_RIFF) + throw new IOException("File is not a WAVE file"); + + // skip size + in.readInt(); + if (in.readInt() != i_WAVE) + throw new IOException("WAVE File does not contain audio"); + + readStream = stream; + if (readStream){ + audioStream = new AudioStream(); + audioData = audioStream; + }else{ + audioBuffer = new AudioBuffer(); + audioData = audioBuffer; + } + + while (true) { + int type = in.readInt(); + int len = in.readInt(); + + switch (type) { + case i_fmt: + readFormatChunk(len); + break; + case i_data: + // Compute duration based on data chunk size + duration = len / bytesPerSec; + + if (readStream) { + readDataChunkForStream(len); + } else { + readDataChunkForBuffer(len); + } + return audioData; + default: + int skipped = in.skipBytes(len); + if (skipped <= 0) { + return null; + } + break; + } + } + } + + public Object load(AssetInfo info) throws IOException { + AudioData data; + InputStream inputStream = null; + try { + inputStream = info.openStream(); + data = load(inputStream, ((AudioKey)info.getKey()).isStream()); + if (data instanceof AudioStream){ + inputStream = null; + } + return data; + } finally { + if (inputStream != null){ + inputStream.close(); + } + } + } +} diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java new file mode 100644 index 0000000..20da0e7 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +class BinaryClassField { + + public static final byte BYTE = 0; + public static final byte BYTE_1D = 1; + public static final byte BYTE_2D = 2; + + public static final byte INT = 10; + public static final byte INT_1D = 11; + public static final byte INT_2D = 12; + + public static final byte FLOAT = 20; + public static final byte FLOAT_1D = 21; + public static final byte FLOAT_2D = 22; + + public static final byte DOUBLE = 30; + public static final byte DOUBLE_1D = 31; + public static final byte DOUBLE_2D = 32; + + public static final byte LONG = 40; + public static final byte LONG_1D = 41; + public static final byte LONG_2D = 42; + + public static final byte SHORT = 50; + public static final byte SHORT_1D = 51; + public static final byte SHORT_2D = 52; + + public static final byte BOOLEAN = 60; + public static final byte BOOLEAN_1D = 61; + public static final byte BOOLEAN_2D = 62; + + public static final byte STRING = 70; + public static final byte STRING_1D = 71; + public static final byte STRING_2D = 72; + + public static final byte BITSET = 80; + + public static final byte SAVABLE = 90; + public static final byte SAVABLE_1D = 91; + public static final byte SAVABLE_2D = 92; + + public static final byte SAVABLE_ARRAYLIST = 100; + public static final byte SAVABLE_ARRAYLIST_1D = 101; + public static final byte SAVABLE_ARRAYLIST_2D = 102; + + public static final byte SAVABLE_MAP = 105; + public static final byte STRING_SAVABLE_MAP = 106; + public static final byte INT_SAVABLE_MAP = 107; + + public static final byte FLOATBUFFER_ARRAYLIST = 110; + public static final byte BYTEBUFFER_ARRAYLIST = 111; + + public static final byte FLOATBUFFER = 120; + public static final byte INTBUFFER = 121; + public static final byte BYTEBUFFER = 122; + public static final byte SHORTBUFFER = 123; + + + byte type; + String name; + byte alias; + + BinaryClassField(String name, byte alias, byte type) { + this.name = name; + this.alias = alias; + this.type = type; + } +} diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java new file mode 100644 index 0000000..e66a945 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +import java.util.HashMap; + +class BinaryClassObject { + + // When exporting, use nameFields field, importing use aliasFields. + HashMap<String, BinaryClassField> nameFields; + HashMap<Byte, BinaryClassField> aliasFields; + + byte[] alias; + String className; + int[] classHierarchyVersions; +} diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java new file mode 100644 index 0000000..b9ec78d --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +import com.jme3.export.FormatVersion; +import com.jme3.export.JmeExporter; +import com.jme3.export.Savable; +import com.jme3.export.SavableClassUtil; +import com.jme3.math.FastMath; +import java.io.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Exports to the jME Binary Format. Format descriptor: (each numbered item + * denotes a series of bytes that follows sequentially one after the next.) + * <p> + * 1. "number of classes" - four bytes - int value representing the number of + * entries in the class lookup table. + * </p> + * <p> + * CLASS TABLE: There will be X blocks each consisting of numbers 2 thru 9, + * where X = the number read in 1. + * </p> + * <p> + * 2. "class alias" - 1...X bytes, where X = ((int) FastMath.log(aliasCount, + * 256) + 1) - an alias used when writing object data to match an object to its + * appropriate object class type. + * </p> + * <p> + * 3. "full class name size" - four bytes - int value representing number of + * bytes to read in for next field. + * </p> + * <p> + * 4. "full class name" - 1...X bytes representing a String value, where X = the + * number read in 3. The String is the fully qualified class name of the Savable + * class, eg "<code>com.jme.math.Vector3f</code>" + * </p> + * <p> + * 5. "number of fields" - four bytes - int value representing number of blocks + * to read in next (numbers 6 - 9), where each block represents a field in this + * class. + * </p> + * <p> + * 6. "field alias" - 1 byte - the alias used when writing out fields in a + * class. Because it is a single byte, a single class can not save out more than + * a total of 256 fields. + * </p> + * <p> + * 7. "field type" - 1 byte - a value representing the type of data a field + * contains. This value is taken from the static fields of + * <code>com.jme.util.export.binary.BinaryClassField</code>. + * </p> + * <p> + * 8. "field name size" - 4 bytes - int value representing the size of the next + * field. + * </p> + * <p> + * 9. "field name" - 1...X bytes representing a String value, where X = the + * number read in 8. The String is the full String value used when writing the + * current field. + * </p> + * <p> + * 10. "number of unique objects" - four bytes - int value representing the + * number of data entries in this file. + * </p> + * <p> + * DATA LOOKUP TABLE: There will be X blocks each consisting of numbers 11 and + * 12, where X = the number read in 10. + * </p> + * <p> + * 11. "data id" - four bytes - int value identifying a single unique object + * that was saved in this data file. + * </p> + * <p> + * 12. "data location" - four bytes - int value representing the offset in the + * object data portion of this file where the object identified in 11 is + * located. + * </p> + * <p> + * 13. "future use" - four bytes - hardcoded int value 1. + * </p> + * <p> + * 14. "root id" - four bytes - int value identifying the top level object. + * </p> + * <p> + * OBJECT DATA SECTION: There will be X blocks each consisting of numbers 15 + * thru 19, where X = the number of unique location values named in 12. + * <p> + * 15. "class alias" - see 2. + * </p> + * <p> + * 16. "data length" - four bytes - int value representing the length in bytes + * of data stored in fields 17 and 18 for this object. + * </p> + * <p> + * FIELD ENTRY: There will be X blocks each consisting of numbers 18 and 19 + * </p> + * <p> + * 17. "field alias" - see 6. + * </p> + * <p> + * 18. "field data" - 1...X bytes representing the field data. The data length + * is dependent on the field type and contents. + * </p> + * + * @author Joshua Slack + */ + +public class BinaryExporter implements JmeExporter { + private static final Logger logger = Logger.getLogger(BinaryExporter.class + .getName()); + + protected int aliasCount = 1; + protected int idCount = 1; + + protected IdentityHashMap<Savable, BinaryIdContentPair> contentTable + = new IdentityHashMap<Savable, BinaryIdContentPair>(); + + protected HashMap<Integer, Integer> locationTable + = new HashMap<Integer, Integer>(); + + // key - class name, value = bco + private HashMap<String, BinaryClassObject> classes + = new HashMap<String, BinaryClassObject>(); + + private ArrayList<Savable> contentKeys = new ArrayList<Savable>(); + + public static boolean debug = false; + public static boolean useFastBufs = true; + + public BinaryExporter() { + } + + public static BinaryExporter getInstance() { + return new BinaryExporter(); + } + + public boolean save(Savable object, OutputStream os) throws IOException { + // reset some vars + aliasCount = 1; + idCount = 1; + classes.clear(); + contentTable.clear(); + locationTable.clear(); + contentKeys.clear(); + + // write signature and version + os.write(ByteUtils.convertToBytes(FormatVersion.SIGNATURE)); + os.write(ByteUtils.convertToBytes(FormatVersion.VERSION)); + + int id = processBinarySavable(object); + + // write out tag table + int classTableSize = 0; + int classNum = classes.keySet().size(); + int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all + // aliases a + // fixed width + + os.write(ByteUtils.convertToBytes(classNum)); + for (String key : classes.keySet()) { + BinaryClassObject bco = classes.get(key); + + // write alias + byte[] aliasBytes = fixClassAlias(bco.alias, + aliasSize); + os.write(aliasBytes); + classTableSize += aliasSize; + + // jME3 NEW: Write class hierarchy version numbers + os.write( bco.classHierarchyVersions.length ); + for (int version : bco.classHierarchyVersions){ + os.write(ByteUtils.convertToBytes(version)); + } + classTableSize += 1 + bco.classHierarchyVersions.length * 4; + + // write classname size & classname + byte[] classBytes = key.getBytes(); + os.write(ByteUtils.convertToBytes(classBytes.length)); + os.write(classBytes); + classTableSize += 4 + classBytes.length; + + // for each field, write alias, type, and name + os.write(ByteUtils.convertToBytes(bco.nameFields.size())); + for (String fieldName : bco.nameFields.keySet()) { + BinaryClassField bcf = bco.nameFields.get(fieldName); + os.write(bcf.alias); + os.write(bcf.type); + + // write classname size & classname + byte[] fNameBytes = fieldName.getBytes(); + os.write(ByteUtils.convertToBytes(fNameBytes.length)); + os.write(fNameBytes); + classTableSize += 2 + 4 + fNameBytes.length; + } + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // write out data to a seperate stream + int location = 0; + // keep track of location for each piece + HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<String, ArrayList<BinaryIdContentPair>>( + contentTable.size()); + for (Savable savable : contentKeys) { + // look back at previous written data for matches + String savableName = savable.getClass().getName(); + BinaryIdContentPair pair = contentTable.get(savable); + ArrayList<BinaryIdContentPair> bucket = alreadySaved + .get(savableName + getChunk(pair)); + int prevLoc = findPrevMatch(pair, bucket); + if (prevLoc != -1) { + locationTable.put(pair.getId(), prevLoc); + continue; + } + + locationTable.put(pair.getId(), location); + if (bucket == null) { + bucket = new ArrayList<BinaryIdContentPair>(); + alreadySaved.put(savableName + getChunk(pair), bucket); + } + bucket.add(pair); + byte[] aliasBytes = fixClassAlias(classes.get(savableName).alias, aliasSize); + out.write(aliasBytes); + location += aliasSize; + BinaryOutputCapsule cap = contentTable.get(savable).getContent(); + out.write(ByteUtils.convertToBytes(cap.bytes.length)); + location += 4; // length of bytes + out.write(cap.bytes); + location += cap.bytes.length; + } + + // write out location table + // tag/location + int numLocations = locationTable.keySet().size(); + os.write(ByteUtils.convertToBytes(numLocations)); + int locationTableSize = 0; + for (Integer key : locationTable.keySet()) { + os.write(ByteUtils.convertToBytes(key)); + os.write(ByteUtils.convertToBytes(locationTable.get(key))); + locationTableSize += 8; + } + + // write out number of root ids - hardcoded 1 for now + os.write(ByteUtils.convertToBytes(1)); + + // write out root id + os.write(ByteUtils.convertToBytes(id)); + + // append stream to the output stream + out.writeTo(os); + + + out = null; + os = null; + + if (debug ) { + logger.info("Stats:"); + logger.log(Level.INFO, "classes: {0}", classNum); + logger.log(Level.INFO, "class table: {0} bytes", classTableSize); + logger.log(Level.INFO, "objects: {0}", numLocations); + logger.log(Level.INFO, "location table: {0} bytes", locationTableSize); + logger.log(Level.INFO, "data: {0} bytes", location); + } + + return true; + } + + protected String getChunk(BinaryIdContentPair pair) { + return new String(pair.getContent().bytes, 0, Math.min(64, pair + .getContent().bytes.length)); + } + + protected int findPrevMatch(BinaryIdContentPair oldPair, + ArrayList<BinaryIdContentPair> bucket) { + if (bucket == null) + return -1; + for (int x = bucket.size(); --x >= 0;) { + BinaryIdContentPair pair = bucket.get(x); + if (pair.getContent().equals(oldPair.getContent())) + return locationTable.get(pair.getId()); + } + return -1; + } + + protected byte[] fixClassAlias(byte[] bytes, int width) { + if (bytes.length != width) { + byte[] newAlias = new byte[width]; + for (int x = width - bytes.length; x < width; x++) + newAlias[x] = bytes[x - bytes.length]; + return newAlias; + } + return bytes; + } + + public boolean save(Savable object, File f) throws IOException { + File parentDirectory = f.getParentFile(); + if(parentDirectory != null && !parentDirectory.exists()) { + parentDirectory.mkdirs(); + } + + FileOutputStream fos = new FileOutputStream(f); + boolean rVal = save(object, fos); + fos.close(); + return rVal; + } + + public BinaryOutputCapsule getCapsule(Savable object) { + return contentTable.get(object).getContent(); + } + + private BinaryClassObject createClassObject(Class clazz) throws IOException{ + BinaryClassObject bco = new BinaryClassObject(); + bco.alias = generateTag(); + bco.nameFields = new HashMap<String, BinaryClassField>(); + bco.classHierarchyVersions = SavableClassUtil.getSavableVersions(clazz); + + classes.put(clazz.getName(), bco); + + return bco; + } + + public int processBinarySavable(Savable object) throws IOException { + if (object == null) { + return -1; + } + Class<? extends Savable> clazz = object.getClass(); + BinaryClassObject bco = classes.get(object.getClass().getName()); + // is this class been looked at before? in tagTable? + if (bco == null) { + bco = createClassObject(object.getClass()); + } + + // is object in contentTable? + if (contentTable.get(object) != null) { + return (contentTable.get(object).getId()); + } + BinaryIdContentPair newPair = generateIdContentPair(bco); + BinaryIdContentPair old = contentTable.put(object, newPair); + if (old == null) { + contentKeys.add(object); + } + object.write(this); + newPair.getContent().finish(); + return newPair.getId(); + + } + + protected byte[] generateTag() { + int width = ((int) FastMath.log(aliasCount, 256) + 1); + int count = aliasCount; + aliasCount++; + byte[] bytes = new byte[width]; + for (int x = width - 1; x >= 0; x--) { + int pow = (int) FastMath.pow(256, x); + int factor = count / pow; + bytes[width - x - 1] = (byte) factor; + count %= pow; + } + return bytes; + } + + protected BinaryIdContentPair generateIdContentPair(BinaryClassObject bco) { + BinaryIdContentPair pair = new BinaryIdContentPair(idCount++, + new BinaryOutputCapsule(this, bco)); + return pair; + } +}
\ No newline at end of file diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java new file mode 100644 index 0000000..b999a3c --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +class BinaryIdContentPair { + + private int id; + private BinaryOutputCapsule content; + + BinaryIdContentPair(int id, BinaryOutputCapsule content) { + this.id = id; + this.content = content; + } + + BinaryOutputCapsule getContent() { + return content; + } + + void setContent(BinaryOutputCapsule content) { + this.content = content; + } + + int getId() { + return id; + } + + void setId(int id) { + this.id = id; + } +} diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java new file mode 100644 index 0000000..7f94ba4 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetManager; +import com.jme3.export.*; +import com.jme3.math.FastMath; +import java.io.*; +import java.net.URL; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Joshua Slack + * @author Kirill Vainer - Version number, Fast buffer reading + */ +public final class BinaryImporter implements JmeImporter { + private static final Logger logger = Logger.getLogger(BinaryImporter.class + .getName()); + + private AssetManager assetManager; + + //Key - alias, object - bco + private HashMap<String, BinaryClassObject> classes + = new HashMap<String, BinaryClassObject>(); + //Key - id, object - the savable + private HashMap<Integer, Savable> contentTable + = new HashMap<Integer, Savable>(); + //Key - savable, object - capsule + private IdentityHashMap<Savable, BinaryInputCapsule> capsuleTable + = new IdentityHashMap<Savable, BinaryInputCapsule>(); + //Key - id, opject - location in the file + private HashMap<Integer, Integer> locationTable + = new HashMap<Integer, Integer>(); + + public static boolean debug = false; + + private byte[] dataArray; + private int aliasWidth; + private int formatVersion; + + private static final boolean fastRead = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; + + public BinaryImporter() { + } + + public int getFormatVersion(){ + return formatVersion; + } + + public static boolean canUseFastBuffers(){ + return fastRead; + } + + public static BinaryImporter getInstance() { + return new BinaryImporter(); + } + + public void setAssetManager(AssetManager manager){ + this.assetManager = manager; + } + + public AssetManager getAssetManager(){ + return assetManager; + } + + public Object load(AssetInfo info){ +// if (!(info.getKey() instanceof ModelKey)) +// throw new IllegalArgumentException("Model assets must be loaded using a ModelKey"); + + assetManager = info.getManager(); + + InputStream is = null; + try { + is = info.openStream(); + Savable s = load(is); + + return s; + } catch (IOException ex) { + logger.log(Level.SEVERE, "An error occured while loading jME binary object", ex); + } finally { + if (is != null){ + try { + is.close(); + } catch (IOException ex) {} + } + } + return null; + } + + public Savable load(InputStream is) throws IOException { + return load(is, null, null); + } + + public Savable load(InputStream is, ReadListener listener) throws IOException { + return load(is, listener, null); + } + + public Savable load(InputStream is, ReadListener listener, ByteArrayOutputStream baos) throws IOException { + contentTable.clear(); + BufferedInputStream bis = new BufferedInputStream(is); + + int numClasses; + + // Try to read signature + int maybeSignature = ByteUtils.readInt(bis); + if (maybeSignature == FormatVersion.SIGNATURE){ + // this is a new version J3O file + formatVersion = ByteUtils.readInt(bis); + numClasses = ByteUtils.readInt(bis); + + // check if this binary is from the future + if (formatVersion > FormatVersion.VERSION){ + throw new IOException("The binary file is of newer version than expected! " + + formatVersion + " > " + FormatVersion.VERSION); + } + }else{ + // this is an old version J3O file + // the signature was actually the class count + numClasses = maybeSignature; + + // 0 indicates version before we started adding + // version numbers + formatVersion = 0; + } + + int bytes = 4; + aliasWidth = ((int)FastMath.log(numClasses, 256) + 1); + + classes.clear(); + for(int i = 0; i < numClasses; i++) { + String alias = readString(bis, aliasWidth); + + // jME3 NEW: Read class version number + int[] classHierarchyVersions; + if (formatVersion >= 1){ + int classHierarchySize = bis.read(); + classHierarchyVersions = new int[classHierarchySize]; + for (int j = 0; j < classHierarchySize; j++){ + classHierarchyVersions[j] = ByteUtils.readInt(bis); + } + }else{ + classHierarchyVersions = new int[]{ 0 }; + } + + // read classname and classname size + int classLength = ByteUtils.readInt(bis); + String className = readString(bis, classLength); + + BinaryClassObject bco = new BinaryClassObject(); + bco.alias = alias.getBytes(); + bco.className = className; + bco.classHierarchyVersions = classHierarchyVersions; + + int fields = ByteUtils.readInt(bis); + bytes += (8 + aliasWidth + classLength); + + bco.nameFields = new HashMap<String, BinaryClassField>(fields); + bco.aliasFields = new HashMap<Byte, BinaryClassField>(fields); + for (int x = 0; x < fields; x++) { + byte fieldAlias = (byte)bis.read(); + byte fieldType = (byte)bis.read(); + + int fieldNameLength = ByteUtils.readInt(bis); + String fieldName = readString(bis, fieldNameLength); + BinaryClassField bcf = new BinaryClassField(fieldName, fieldAlias, fieldType); + bco.nameFields.put(fieldName, bcf); + bco.aliasFields.put(fieldAlias, bcf); + bytes += (6 + fieldNameLength); + } + classes.put(alias, bco); + } + if (listener != null) listener.readBytes(bytes); + + int numLocs = ByteUtils.readInt(bis); + bytes = 4; + + capsuleTable.clear(); + locationTable.clear(); + for(int i = 0; i < numLocs; i++) { + int id = ByteUtils.readInt(bis); + int loc = ByteUtils.readInt(bis); + locationTable.put(id, loc); + bytes += 8; + } + + @SuppressWarnings("unused") + int numbIDs = ByteUtils.readInt(bis); // XXX: NOT CURRENTLY USED + int id = ByteUtils.readInt(bis); + bytes += 8; + if (listener != null) listener.readBytes(bytes); + + if (baos == null) { + baos = new ByteArrayOutputStream(bytes); + } else { + baos.reset(); + } + int size = -1; + byte[] cache = new byte[4096]; + while((size = bis.read(cache)) != -1) { + baos.write(cache, 0, size); + if (listener != null) listener.readBytes(size); + } + bis = null; + + dataArray = baos.toByteArray(); + baos = null; + + Savable rVal = readObject(id); + if (debug) { + logger.info("Importer Stats: "); + logger.log(Level.INFO, "Tags: {0}", numClasses); + logger.log(Level.INFO, "Objects: {0}", numLocs); + logger.log(Level.INFO, "Data Size: {0}", dataArray.length); + } + dataArray = null; + return rVal; + } + + public Savable load(URL f) throws IOException { + return load(f, null); + } + + public Savable load(URL f, ReadListener listener) throws IOException { + InputStream is = f.openStream(); + Savable rVal = load(is, listener); + is.close(); + return rVal; + } + + public Savable load(File f) throws IOException { + return load(f, null); + } + + public Savable load(File f, ReadListener listener) throws IOException { + FileInputStream fis = new FileInputStream(f); + Savable rVal = load(fis, listener); + fis.close(); + return rVal; + } + + public Savable load(byte[] data) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(data); + Savable rVal = load(bais); + bais.close(); + return rVal; + } + + @Override + public InputCapsule getCapsule(Savable id) { + return capsuleTable.get(id); + } + + protected String readString(InputStream f, int length) throws IOException { + byte[] data = new byte[length]; + for(int j = 0; j < length; j++) { + data[j] = (byte)f.read(); + } + + return new String(data); + } + + protected String readString(int length, int offset) throws IOException { + byte[] data = new byte[length]; + for(int j = 0; j < length; j++) { + data[j] = dataArray[j+offset]; + } + + return new String(data); + } + + public Savable readObject(int id) { + + if(contentTable.get(id) != null) { + return contentTable.get(id); + } + + try { + int loc = locationTable.get(id); + + String alias = readString(aliasWidth, loc); + loc+=aliasWidth; + + BinaryClassObject bco = classes.get(alias); + + if(bco == null) { + logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "NULL class object: " + alias); + return null; + } + + int dataLength = ByteUtils.convertIntFromBytes(dataArray, loc); + loc+=4; + + Savable out = null; + if (assetManager != null) { + out = SavableClassUtil.fromName(bco.className, assetManager.getClassLoaders()); + } else { + out = SavableClassUtil.fromName(bco.className); + } + + BinaryInputCapsule cap = new BinaryInputCapsule(this, out, bco); + cap.setContent(dataArray, loc, loc+dataLength); + + capsuleTable.put(out, cap); + contentTable.put(id, out); + + out.read(this); + + capsuleTable.remove(out); + + return out; + + } catch (IOException e) { + logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e); + return null; + } catch (ClassNotFoundException e) { + logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e); + return null; + } catch (InstantiationException e) { + logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e); + return null; + } catch (IllegalAccessException e) { + logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e); + return null; + } + } +}
\ No newline at end of file diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java new file mode 100644 index 0000000..cebd764 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java @@ -0,0 +1,1380 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +import com.jme3.export.InputCapsule; +import com.jme3.export.Savable; +import com.jme3.export.SavableClassUtil; +import com.jme3.util.BufferUtils; +import com.jme3.util.IntMap; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Joshua Slack + */ +final class BinaryInputCapsule implements InputCapsule { + + private static final Logger logger = Logger + .getLogger(BinaryInputCapsule.class.getName()); + + protected BinaryImporter importer; + protected BinaryClassObject cObj; + protected Savable savable; + protected HashMap<Byte, Object> fieldData; + + protected int index = 0; + + public BinaryInputCapsule(BinaryImporter importer, Savable savable, BinaryClassObject bco) { + this.importer = importer; + this.cObj = bco; + this.savable = savable; + } + + public void setContent(byte[] content, int start, int limit) { + fieldData = new HashMap<Byte, Object>(); + for (index = start; index < limit;) { + byte alias = content[index]; + + index++; + + try { + byte type = cObj.aliasFields.get(alias).type; + Object value = null; + + switch (type) { + case BinaryClassField.BITSET: { + value = readBitSet(content); + break; + } + case BinaryClassField.BOOLEAN: { + value = readBoolean(content); + break; + } + case BinaryClassField.BOOLEAN_1D: { + value = readBooleanArray(content); + break; + } + case BinaryClassField.BOOLEAN_2D: { + value = readBooleanArray2D(content); + break; + } + case BinaryClassField.BYTE: { + value = readByte(content); + break; + } + case BinaryClassField.BYTE_1D: { + value = readByteArray(content); + break; + } + case BinaryClassField.BYTE_2D: { + value = readByteArray2D(content); + break; + } + case BinaryClassField.BYTEBUFFER: { + value = readByteBuffer(content); + break; + } + case BinaryClassField.DOUBLE: { + value = readDouble(content); + break; + } + case BinaryClassField.DOUBLE_1D: { + value = readDoubleArray(content); + break; + } + case BinaryClassField.DOUBLE_2D: { + value = readDoubleArray2D(content); + break; + } + case BinaryClassField.FLOAT: { + value = readFloat(content); + break; + } + case BinaryClassField.FLOAT_1D: { + value = readFloatArray(content); + break; + } + case BinaryClassField.FLOAT_2D: { + value = readFloatArray2D(content); + break; + } + case BinaryClassField.FLOATBUFFER: { + value = readFloatBuffer(content); + break; + } + case BinaryClassField.FLOATBUFFER_ARRAYLIST: { + value = readFloatBufferArrayList(content); + break; + } + case BinaryClassField.BYTEBUFFER_ARRAYLIST: { + value = readByteBufferArrayList(content); + break; + } + case BinaryClassField.INT: { + value = readInt(content); + break; + } + case BinaryClassField.INT_1D: { + value = readIntArray(content); + break; + } + case BinaryClassField.INT_2D: { + value = readIntArray2D(content); + break; + } + case BinaryClassField.INTBUFFER: { + value = readIntBuffer(content); + break; + } + case BinaryClassField.LONG: { + value = readLong(content); + break; + } + case BinaryClassField.LONG_1D: { + value = readLongArray(content); + break; + } + case BinaryClassField.LONG_2D: { + value = readLongArray2D(content); + break; + } + case BinaryClassField.SAVABLE: { + value = readSavable(content); + break; + } + case BinaryClassField.SAVABLE_1D: { + value = readSavableArray(content); + break; + } + case BinaryClassField.SAVABLE_2D: { + value = readSavableArray2D(content); + break; + } + case BinaryClassField.SAVABLE_ARRAYLIST: { + value = readSavableArray(content); + break; + } + case BinaryClassField.SAVABLE_ARRAYLIST_1D: { + value = readSavableArray2D(content); + break; + } + case BinaryClassField.SAVABLE_ARRAYLIST_2D: { + value = readSavableArray3D(content); + break; + } + case BinaryClassField.SAVABLE_MAP: { + value = readSavableMap(content); + break; + } + case BinaryClassField.STRING_SAVABLE_MAP: { + value = readStringSavableMap(content); + break; + } + case BinaryClassField.INT_SAVABLE_MAP: { + value = readIntSavableMap(content); + break; + } + case BinaryClassField.SHORT: { + value = readShort(content); + break; + } + case BinaryClassField.SHORT_1D: { + value = readShortArray(content); + break; + } + case BinaryClassField.SHORT_2D: { + value = readShortArray2D(content); + break; + } + case BinaryClassField.SHORTBUFFER: { + value = readShortBuffer(content); + break; + } + case BinaryClassField.STRING: { + value = readString(content); + break; + } + case BinaryClassField.STRING_1D: { + value = readStringArray(content); + break; + } + case BinaryClassField.STRING_2D: { + value = readStringArray2D(content); + break; + } + + default: + // skip put statement + continue; + } + + fieldData.put(alias, value); + + } catch (IOException e) { + logger.logp(Level.SEVERE, this.getClass().toString(), + "setContent(byte[] content)", "Exception", e); + } + } + } + + public int getSavableVersion(Class<? extends Savable> desiredClass){ + return SavableClassUtil.getSavedSavableVersion(savable, desiredClass, + cObj.classHierarchyVersions, importer.getFormatVersion()); + } + + public BitSet readBitSet(String name, BitSet defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (BitSet) fieldData.get(field.alias); + } + + public boolean readBoolean(String name, boolean defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Boolean) fieldData.get(field.alias)).booleanValue(); + } + + public boolean[] readBooleanArray(String name, boolean[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (boolean[]) fieldData.get(field.alias); + } + + public boolean[][] readBooleanArray2D(String name, boolean[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (boolean[][]) fieldData.get(field.alias); + } + + public byte readByte(String name, byte defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Byte) fieldData.get(field.alias)).byteValue(); + } + + public byte[] readByteArray(String name, byte[] defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (byte[]) fieldData.get(field.alias); + } + + public byte[][] readByteArray2D(String name, byte[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (byte[][]) fieldData.get(field.alias); + } + + public ByteBuffer readByteBuffer(String name, ByteBuffer defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (ByteBuffer) fieldData.get(field.alias); + } + + @SuppressWarnings("unchecked") + public ArrayList<ByteBuffer> readByteBufferArrayList(String name, + ArrayList<ByteBuffer> defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (ArrayList<ByteBuffer>) fieldData.get(field.alias); + } + + public double readDouble(String name, double defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Double) fieldData.get(field.alias)).doubleValue(); + } + + public double[] readDoubleArray(String name, double[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (double[]) fieldData.get(field.alias); + } + + public double[][] readDoubleArray2D(String name, double[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (double[][]) fieldData.get(field.alias); + } + + public float readFloat(String name, float defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Float) fieldData.get(field.alias)).floatValue(); + } + + public float[] readFloatArray(String name, float[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (float[]) fieldData.get(field.alias); + } + + public float[][] readFloatArray2D(String name, float[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (float[][]) fieldData.get(field.alias); + } + + public FloatBuffer readFloatBuffer(String name, FloatBuffer defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (FloatBuffer) fieldData.get(field.alias); + } + + @SuppressWarnings("unchecked") + public ArrayList<FloatBuffer> readFloatBufferArrayList(String name, + ArrayList<FloatBuffer> defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (ArrayList<FloatBuffer>) fieldData.get(field.alias); + } + + public int readInt(String name, int defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Integer) fieldData.get(field.alias)).intValue(); + } + + public int[] readIntArray(String name, int[] defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (int[]) fieldData.get(field.alias); + } + + public int[][] readIntArray2D(String name, int[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (int[][]) fieldData.get(field.alias); + } + + public IntBuffer readIntBuffer(String name, IntBuffer defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (IntBuffer) fieldData.get(field.alias); + } + + public long readLong(String name, long defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Long) fieldData.get(field.alias)).longValue(); + } + + public long[] readLongArray(String name, long[] defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (long[]) fieldData.get(field.alias); + } + + public long[][] readLongArray2D(String name, long[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (long[][]) fieldData.get(field.alias); + } + + public Savable readSavable(String name, Savable defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value == null) + return null; + else if (value instanceof ID) { + value = importer.readObject(((ID) value).id); + fieldData.put(field.alias, value); + return (Savable) value; + } else + return defVal; + } + + public Savable[] readSavableArray(String name, Savable[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object[] values = (Object[]) fieldData.get(field.alias); + if (values instanceof ID[]) { + values = resolveIDs(values); + fieldData.put(field.alias, values); + return (Savable[]) values; + } else + return defVal; + } + + private Savable[] resolveIDs(Object[] values) { + if (values != null) { + Savable[] savables = new Savable[values.length]; + for (int i = 0; i < values.length; i++) { + final ID id = (ID) values[i]; + savables[i] = id != null ? importer.readObject(id.id) : null; + } + return savables; + } else { + return null; + } + } + + public Savable[][] readSavableArray2D(String name, Savable[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null ||!fieldData.containsKey(field.alias)) + return defVal; + Object[][] values = (Object[][]) fieldData.get(field.alias); + if (values instanceof ID[][]) { + Savable[][] savables = new Savable[values.length][]; + for (int i = 0; i < values.length; i++) { + if (values[i] != null) { + savables[i] = resolveIDs(values[i]); + } else savables[i] = null; + } + values = savables; + fieldData.put(field.alias, values); + } + return (Savable[][]) values; + } + + public Savable[][][] readSavableArray3D(String name, Savable[][][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object[][][] values = (Object[][][]) fieldData.get(field.alias); + if (values instanceof ID[][][]) { + Savable[][][] savables = new Savable[values.length][][]; + for (int i = 0; i < values.length; i++) { + if (values[i] != null) { + savables[i] = new Savable[values[i].length][]; + for (int j = 0; j < values[i].length; j++) { + savables[i][j] = resolveIDs(values[i][j]); + } + } else savables[i] = null; + } + fieldData.put(field.alias, savables); + return savables; + } else + return defVal; + } + + private ArrayList<Savable> savableArrayListFromArray(Savable[] savables) { + if(savables == null) { + return null; + } + ArrayList<Savable> arrayList = new ArrayList<Savable>(savables.length); + for (int x = 0; x < savables.length; x++) { + arrayList.add(savables[x]); + } + return arrayList; + } + + // Assumes array of size 2 arrays where pos 0 is key and pos 1 is value. + private Map<Savable, Savable> savableMapFrom2DArray(Savable[][] savables) { + if(savables == null) { + return null; + } + Map<Savable, Savable> map = new HashMap<Savable, Savable>(savables.length); + for (int x = 0; x < savables.length; x++) { + map.put(savables[x][0], savables[x][1]); + } + return map; + } + + private Map<String, Savable> stringSavableMapFromKV(String[] keys, Savable[] values) { + if(keys == null || values == null) { + return null; + } + + Map<String, Savable> map = new HashMap<String, Savable>(keys.length); + for (int x = 0; x < keys.length; x++) + map.put(keys[x], values[x]); + + return map; + } + + private IntMap<Savable> intSavableMapFromKV(int[] keys, Savable[] values) { + if(keys == null || values == null) { + return null; + } + + IntMap<Savable> map = new IntMap<Savable>(keys.length); + for (int x = 0; x < keys.length; x++) + map.put(keys[x], values[x]); + + return map; + } + + public ArrayList readSavableArrayList(String name, ArrayList defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value instanceof ID[]) { + // read Savable array and convert to ArrayList + Savable[] savables = readSavableArray(name, null); + value = savableArrayListFromArray(savables); + fieldData.put(field.alias, value); + } + return (ArrayList) value; + } + + public ArrayList[] readSavableArrayListArray(String name, ArrayList[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value instanceof ID[][]) { + // read 2D Savable array and convert to ArrayList array + Savable[][] savables = readSavableArray2D(name, null); + if (savables != null) { + ArrayList[] arrayLists = new ArrayList[savables.length]; + for (int i = 0; i < savables.length; i++) { + arrayLists[i] = savableArrayListFromArray(savables[i]); + } + value = arrayLists; + } else + value = defVal; + fieldData.put(field.alias, value); + } + return (ArrayList[]) value; + } + + public ArrayList[][] readSavableArrayListArray2D(String name, + ArrayList[][] defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value instanceof ID[][][]) { + // read 3D Savable array and convert to 2D ArrayList array + Savable[][][] savables = readSavableArray3D(name, null); + if (savables != null && savables.length > 0) { + ArrayList[][] arrayLists = new ArrayList[savables.length][]; + for (int i = 0; i < savables.length; i++) { + arrayLists[i] = new ArrayList[savables[i].length]; + for (int j = 0; j < savables[i].length; j++) { + arrayLists[i][j] = savableArrayListFromArray(savables[i][j]); + } + } + value = arrayLists; + } else + value = defVal; + fieldData.put(field.alias, value); + } + return (ArrayList[][]) value; + } + + @SuppressWarnings("unchecked") + public Map<? extends Savable, ? extends Savable> readSavableMap(String name, Map<? extends Savable, ? extends Savable> defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value instanceof ID[][]) { + // read Savable array and convert to Map + Savable[][] savables = readSavableArray2D(name, null); + value = savableMapFrom2DArray(savables); + fieldData.put(field.alias, value); + } + return (Map<? extends Savable, ? extends Savable>) value; + } + + @SuppressWarnings("unchecked") + public Map<String, ? extends Savable> readStringSavableMap(String name, Map<String, ? extends Savable> defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value instanceof StringIDMap) { + // read Savable array and convert to Map values + StringIDMap in = (StringIDMap) value; + Savable[] values = resolveIDs(in.values); + value = stringSavableMapFromKV(in.keys, values); + fieldData.put(field.alias, value); + } + return (Map<String, Savable>) value; + } + + @SuppressWarnings("unchecked") + public IntMap<? extends Savable> readIntSavableMap(String name, IntMap<? extends Savable> defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + Object value = fieldData.get(field.alias); + if (value instanceof IntIDMap) { + // read Savable array and convert to Map values + IntIDMap in = (IntIDMap) value; + Savable[] values = resolveIDs(in.values); + value = intSavableMapFromKV(in.keys, values); + fieldData.put(field.alias, value); + } + return (IntMap<Savable>) value; + } + + public short readShort(String name, short defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return ((Short) fieldData.get(field.alias)).shortValue(); + } + + public short[] readShortArray(String name, short[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (short[]) fieldData.get(field.alias); + } + + public short[][] readShortArray2D(String name, short[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (short[][]) fieldData.get(field.alias); + } + + public ShortBuffer readShortBuffer(String name, ShortBuffer defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (ShortBuffer) fieldData.get(field.alias); + } + + public String readString(String name, String defVal) throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (String) fieldData.get(field.alias); + } + + public String[] readStringArray(String name, String[] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (String[]) fieldData.get(field.alias); + } + + public String[][] readStringArray2D(String name, String[][] defVal) + throws IOException { + BinaryClassField field = cObj.nameFields.get(name); + if (field == null || !fieldData.containsKey(field.alias)) + return defVal; + return (String[][]) fieldData.get(field.alias); + } + + // byte primitive + + protected byte readByte(byte[] content) throws IOException { + byte value = content[index]; + index++; + return value; + } + + protected byte readByteForBuffer(byte[] content) throws IOException { + byte value = content[index]; + index++; + return value; + } + + protected byte[] readByteArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + byte[] value = new byte[length]; + for (int x = 0; x < length; x++) + value[x] = readByte(content); + return value; + } + + protected byte[][] readByteArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + byte[][] value = new byte[length][]; + for (int x = 0; x < length; x++) + value[x] = readByteArray(content); + return value; + } + + // int primitive + + protected int readIntForBuffer(byte[] content){ + int number = ((content[index+3] & 0xFF) << 24) + + ((content[index+2] & 0xFF) << 16) + + ((content[index+1] & 0xFF) << 8) + + (content[index] & 0xFF); + index += 4; + return number; + } + + protected int readInt(byte[] content) throws IOException { + byte[] bytes = inflateFrom(content, index); + index += 1 + bytes.length; + bytes = ByteUtils.rightAlignBytes(bytes, 4); + int value = ByteUtils.convertIntFromBytes(bytes); + if (value == BinaryOutputCapsule.NULL_OBJECT + || value == BinaryOutputCapsule.DEFAULT_OBJECT) + index -= 4; + return value; + } + + protected int[] readIntArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + int[] value = new int[length]; + for (int x = 0; x < length; x++) + value[x] = readInt(content); + return value; + } + + protected int[][] readIntArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + int[][] value = new int[length][]; + for (int x = 0; x < length; x++) + value[x] = readIntArray(content); + return value; + } + + // float primitive + + protected float readFloat(byte[] content) throws IOException { + float value = ByteUtils.convertFloatFromBytes(content, index); + index += 4; + return value; + } + + protected float readFloatForBuffer(byte[] content) throws IOException { + int number = readIntForBuffer(content); + return Float.intBitsToFloat(number); + } + + protected float[] readFloatArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + float[] value = new float[length]; + for (int x = 0; x < length; x++) + value[x] = readFloat(content); + return value; + } + + protected float[][] readFloatArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + float[][] value = new float[length][]; + for (int x = 0; x < length; x++) + value[x] = readFloatArray(content); + return value; + } + + // double primitive + + protected double readDouble(byte[] content) throws IOException { + double value = ByteUtils.convertDoubleFromBytes(content, index); + index += 8; + return value; + } + + protected double[] readDoubleArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + double[] value = new double[length]; + for (int x = 0; x < length; x++) + value[x] = readDouble(content); + return value; + } + + protected double[][] readDoubleArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + double[][] value = new double[length][]; + for (int x = 0; x < length; x++) + value[x] = readDoubleArray(content); + return value; + } + + // long primitive + + protected long readLong(byte[] content) throws IOException { + byte[] bytes = inflateFrom(content, index); + index += 1 + bytes.length; + bytes = ByteUtils.rightAlignBytes(bytes, 8); + long value = ByteUtils.convertLongFromBytes(bytes); + return value; + } + + protected long[] readLongArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + long[] value = new long[length]; + for (int x = 0; x < length; x++) + value[x] = readLong(content); + return value; + } + + protected long[][] readLongArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + long[][] value = new long[length][]; + for (int x = 0; x < length; x++) + value[x] = readLongArray(content); + return value; + } + + // short primitive + + protected short readShort(byte[] content) throws IOException { + short value = ByteUtils.convertShortFromBytes(content, index); + index += 2; + return value; + } + + protected short readShortForBuffer(byte[] content) throws IOException { + short number = (short) ((content[index+0] & 0xFF) + + ((content[index+1] & 0xFF) << 8)); + index += 2; + return number; + } + + protected short[] readShortArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + short[] value = new short[length]; + for (int x = 0; x < length; x++) + value[x] = readShort(content); + return value; + } + + protected short[][] readShortArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + short[][] value = new short[length][]; + for (int x = 0; x < length; x++) + value[x] = readShortArray(content); + return value; + } + + // boolean primitive + + protected boolean readBoolean(byte[] content) throws IOException { + boolean value = ByteUtils.convertBooleanFromBytes(content, index); + index += 1; + return value; + } + + protected boolean[] readBooleanArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + boolean[] value = new boolean[length]; + for (int x = 0; x < length; x++) + value[x] = readBoolean(content); + return value; + } + + protected boolean[][] readBooleanArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + boolean[][] value = new boolean[length][]; + for (int x = 0; x < length; x++) + value[x] = readBooleanArray(content); + return value; + } + + /* + * UTF-8 crash course: + * + * UTF-8 codepoints map to UTF-16 codepoints and vv, which is what Java uses for it's Strings. + * (so a UTF-8 codepoint can contain all possible values for a Java char) + * + * A UTF-8 codepoint can be 1, 2 or 3 bytes long. How long a codepint is can be told by reading the first byte: + * b < 0x80, 1 byte + * (b & 0xC0) == 0xC0, 2 bytes + * (b & 0xE0) == 0xE0, 3 bytes + * + * However there is an additional restriction to UTF-8, to enable you to find the start of a UTF-8 codepoint, + * if you start reading at a random point in a UTF-8 byte stream. That's why UTF-8 requires for the second and third byte of + * a multibyte codepoint: + * (b & 0x80) == 0x80 (in other words, first bit must be 1) + */ + private final static int UTF8_START = 0; // next byte should be the start of a new + private final static int UTF8_2BYTE = 2; // next byte should be the second byte of a 2 byte codepoint + private final static int UTF8_3BYTE_1 = 3; // next byte should be the second byte of a 3 byte codepoint + private final static int UTF8_3BYTE_2 = 4; // next byte should be the third byte of a 3 byte codepoint + private final static int UTF8_ILLEGAL = 10; // not an UTF8 string + + // String + protected String readString(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + + /* + * @see ISSUE 276 + * + * We'll transfer the bytes into a seperate byte array. + * While we do that we'll take the opportunity to check if the byte data is valid UTF-8. + * + * If it is not UTF-8 it is most likely saved with the BinaryOutputCapsule bug, that saves Strings using their native + * encoding. Unfortunatly there is no way to know what encoding was used, so we'll parse using the most common one in + * that case; latin-1 aka ISO8859_1 + * + * Encoding of "low" ASCII codepoint (in plain speak: when no special characters are used) will usually look the same + * for UTF-8 and the other 1 byte codepoint encodings (espc true for numbers and regular letters of the alphabet). So these + * are valid UTF-8 and will give the same result (at most a few charakters will appear different, such as the euro sign). + * + * However, when "high" codepoints are used (any codepoint that over 0x7F, in other words where the first bit is a 1) it's + * a different matter and UTF-8 and the 1 byte encoding greatly will differ, as well as most 1 byte encodings relative to each + * other. + * + * It is impossible to detect which one-byte encoding is used. Since UTF8 and practically all 1-byte encodings share the most + * used characters (the "none-high" ones) parsing them will give the same result. However, not all byte sequences are legal in + * UTF-8 (see explantion above). If not UTF-8 encoded content is detected we therefor fallback on latin1. We also log a warning. + * + * By this method we detect all use of 1 byte encoding if they: + * - use a "high" codepoint after a "low" codepoint or a sequence of codepoints that is valid as UTF-8 bytes, that starts with 1000 + * - use a "low" codepoint after a "high" codepoint + * - use a "low" codepoint after "high" codepoint, after a "high" codepoint that starts with 1110 + * + * In practise this means that unless 2 or 3 "high" codepoints are used after each other in proper order, we'll detect the string + * was not originally UTF-8 encoded. + * + */ + byte[] bytes = new byte[length]; + int utf8State = UTF8_START; + int b; + for (int x = 0; x < length; x++) { + bytes[x] = content[index++]; + b = (int) bytes[x] & 0xFF; // unsign our byte + + switch (utf8State) { + case UTF8_START: + if (b < 0x80) { + // good + } + else if ((b & 0xC0) == 0xC0) { + utf8State = UTF8_2BYTE; + } + else if ((b & 0xE0) == 0xE0) { + utf8State = UTF8_3BYTE_1; + } + else { + utf8State = UTF8_ILLEGAL; + } + break; + case UTF8_3BYTE_1: + case UTF8_3BYTE_2: + case UTF8_2BYTE: + if ((b & 0x80) == 0x80) + utf8State = utf8State == UTF8_3BYTE_1 ? UTF8_3BYTE_2 : UTF8_START; + else + utf8State = UTF8_ILLEGAL; + break; + } + } + + try { + // even though so far the parsing might have been a legal UTF-8 sequence, only if a codepoint is fully given is it correct UTF-8 + if (utf8State == UTF8_START) { + // Java misspells UTF-8 as UTF8 for official use in java.lang + return new String(bytes, "UTF8"); + } + else { + logger.log( + Level.WARNING, + "Your export has been saved with an incorrect encoding for it's String fields which means it might not load correctly " + + "due to encoding issues. You should probably re-export your work. See ISSUE 276 in the jME issue tracker." + ); + // We use ISO8859_1 to be consistent across platforms. We could default to native encoding, but this would lead to inconsistent + // behaviour across platforms! + // Developers that have previously saved their exports using the old exporter (wich uses native encoding), can temporarly + // remove the ""ISO8859_1" parameter, and change the above if statement to "if (false)". + // They should then import and re-export their models using the same enviroment they were orginally created in. + return new String(bytes, "ISO8859_1"); + } + } catch (UnsupportedEncodingException uee) { + // as a last resort fall back to platform native. + // JavaDoc is vague about what happens when a decoding a String that contains un undecodable sequence + // it also doesn't specify which encodings have to be supported (though UTF-8 and ISO8859 have been in the SUN JRE since at least 1.1) + logger.log( + Level.SEVERE, + "Your export has been saved with an incorrect encoding or your version of Java is unable to decode the stored string. " + + "While your export may load correctly by falling back, using it on different platforms or java versions might lead to "+ + "very strange inconsitenties. You should probably re-export your work. See ISSUE 276 in the jME issue tracker." + ); + return new String(bytes); + } + } + + protected String[] readStringArray(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + String[] value = new String[length]; + for (int x = 0; x < length; x++) + value[x] = readString(content); + return value; + } + + protected String[][] readStringArray2D(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + String[][] value = new String[length][]; + for (int x = 0; x < length; x++) + value[x] = readStringArray(content); + return value; + } + + // BitSet + + protected BitSet readBitSet(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + BitSet value = new BitSet(length); + for (int x = 0; x < length; x++) + value.set(x, readBoolean(content)); + return value; + } + + // INFLATOR for int and long + + protected static byte[] inflateFrom(byte[] contents, int index) { + byte firstByte = contents[index]; + if (firstByte == BinaryOutputCapsule.NULL_OBJECT) + return ByteUtils.convertToBytes(BinaryOutputCapsule.NULL_OBJECT); + else if (firstByte == BinaryOutputCapsule.DEFAULT_OBJECT) + return ByteUtils.convertToBytes(BinaryOutputCapsule.DEFAULT_OBJECT); + else if (firstByte == 0) + return new byte[0]; + else { + byte[] rVal = new byte[firstByte]; + for (int x = 0; x < rVal.length; x++) + rVal[x] = contents[x + 1 + index]; + return rVal; + } + } + + // BinarySavable + + protected ID readSavable(byte[] content) throws IOException { + int id = readInt(content); + if (id == BinaryOutputCapsule.NULL_OBJECT) { + return null; + } + + return new ID(id); + } + + // BinarySavable array + + protected ID[] readSavableArray(byte[] content) throws IOException { + int elements = readInt(content); + if (elements == BinaryOutputCapsule.NULL_OBJECT) + return null; + ID[] rVal = new ID[elements]; + for (int x = 0; x < elements; x++) { + rVal[x] = readSavable(content); + } + return rVal; + } + + protected ID[][] readSavableArray2D(byte[] content) throws IOException { + int elements = readInt(content); + if (elements == BinaryOutputCapsule.NULL_OBJECT) + return null; + ID[][] rVal = new ID[elements][]; + for (int x = 0; x < elements; x++) { + rVal[x] = readSavableArray(content); + } + return rVal; + } + + protected ID[][][] readSavableArray3D(byte[] content) throws IOException { + int elements = readInt(content); + if (elements == BinaryOutputCapsule.NULL_OBJECT) + return null; + ID[][][] rVal = new ID[elements][][]; + for (int x = 0; x < elements; x++) { + rVal[x] = readSavableArray2D(content); + } + return rVal; + } + + // BinarySavable map + + protected ID[][] readSavableMap(byte[] content) throws IOException { + int elements = readInt(content); + if (elements == BinaryOutputCapsule.NULL_OBJECT) + return null; + ID[][] rVal = new ID[elements][]; + for (int x = 0; x < elements; x++) { + rVal[x] = readSavableArray(content); + } + return rVal; + } + + protected StringIDMap readStringSavableMap(byte[] content) throws IOException { + int elements = readInt(content); + if (elements == BinaryOutputCapsule.NULL_OBJECT) + return null; + String[] keys = readStringArray(content); + ID[] values = readSavableArray(content); + StringIDMap rVal = new StringIDMap(); + rVal.keys = keys; + rVal.values = values; + return rVal; + } + + protected IntIDMap readIntSavableMap(byte[] content) throws IOException { + int elements = readInt(content); + if (elements == BinaryOutputCapsule.NULL_OBJECT) + return null; + int[] keys = readIntArray(content); + ID[] values = readSavableArray(content); + IntIDMap rVal = new IntIDMap(); + rVal.keys = keys; + rVal.values = values; + return rVal; + } + + + // ArrayList<FloatBuffer> + + protected ArrayList<FloatBuffer> readFloatBufferArrayList(byte[] content) + throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) { + return null; + } + ArrayList<FloatBuffer> rVal = new ArrayList<FloatBuffer>(length); + for (int x = 0; x < length; x++) { + rVal.add(readFloatBuffer(content)); + } + return rVal; + } + + // ArrayList<ByteBuffer> + + protected ArrayList<ByteBuffer> readByteBufferArrayList(byte[] content) + throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) { + return null; + } + ArrayList<ByteBuffer> rVal = new ArrayList<ByteBuffer>(length); + for (int x = 0; x < length; x++) { + rVal.add(readByteBuffer(content)); + } + return rVal; + } + + // NIO BUFFERS + // float buffer + + protected FloatBuffer readFloatBuffer(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + + if (BinaryImporter.canUseFastBuffers()){ + ByteBuffer value = BufferUtils.createByteBuffer(length * 4); + value.put(content, index, length * 4).rewind(); + index += length * 4; + return value.asFloatBuffer(); + }else{ + FloatBuffer value = BufferUtils.createFloatBuffer(length); + for (int x = 0; x < length; x++) { + value.put(readFloatForBuffer(content)); + } + value.rewind(); + return value; + } + } + + // int buffer + + protected IntBuffer readIntBuffer(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + + if (BinaryImporter.canUseFastBuffers()){ + ByteBuffer value = BufferUtils.createByteBuffer(length * 4); + value.put(content, index, length * 4).rewind(); + index += length * 4; + return value.asIntBuffer(); + }else{ + IntBuffer value = BufferUtils.createIntBuffer(length); + for (int x = 0; x < length; x++) { + value.put(readIntForBuffer(content)); + } + value.rewind(); + return value; + } + } + + // byte buffer + + protected ByteBuffer readByteBuffer(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + + if (BinaryImporter.canUseFastBuffers()){ + ByteBuffer value = BufferUtils.createByteBuffer(length); + value.put(content, index, length).rewind(); + index += length; + return value; + }else{ + ByteBuffer value = BufferUtils.createByteBuffer(length); + for (int x = 0; x < length; x++) { + value.put(readByteForBuffer(content)); + } + value.rewind(); + return value; + } + } + + // short buffer + + protected ShortBuffer readShortBuffer(byte[] content) throws IOException { + int length = readInt(content); + if (length == BinaryOutputCapsule.NULL_OBJECT) + return null; + + if (BinaryImporter.canUseFastBuffers()){ + ByteBuffer value = BufferUtils.createByteBuffer(length * 2); + value.put(content, index, length * 2).rewind(); + index += length * 2; + return value.asShortBuffer(); + }else{ + ShortBuffer value = BufferUtils.createShortBuffer(length); + for (int x = 0; x < length; x++) { + value.put(readShortForBuffer(content)); + } + value.rewind(); + return value; + } + } + + static private class ID { + public int id; + + public ID(int id) { + this.id = id; + } + } + + static private class StringIDMap { + public String[] keys; + public ID[] values; + } + + static private class IntIDMap { + public int[] keys; + public ID[] values; + } + + public <T extends Enum<T>> T readEnum(String name, Class<T> enumType, T defVal) throws IOException { + String eVal = readString(name, defVal != null ? defVal.name() : null); + if (eVal != null) { + return Enum.valueOf(enumType, eVal); + } else { + return null; + } + } +}
\ No newline at end of file diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java new file mode 100644 index 0000000..4466db5 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java @@ -0,0 +1,944 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +import com.jme3.export.OutputCapsule; +import com.jme3.export.Savable; +import com.jme3.util.IntMap; +import com.jme3.util.IntMap.Entry; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Map; + +/** + * @author Joshua Slack + */ +final class BinaryOutputCapsule implements OutputCapsule { + + public static final int NULL_OBJECT = -1; + public static final int DEFAULT_OBJECT = -2; + + public static byte[] NULL_BYTES = new byte[] { (byte) -1 }; + public static byte[] DEFAULT_BYTES = new byte[] { (byte) -2 }; + + protected ByteArrayOutputStream baos; + protected byte[] bytes; + protected BinaryExporter exporter; + protected BinaryClassObject cObj; + + public BinaryOutputCapsule(BinaryExporter exporter, BinaryClassObject bco) { + this.baos = new ByteArrayOutputStream(); + this.exporter = exporter; + this.cObj = bco; + } + + public void write(byte value, String name, byte defVal) throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BYTE); + write(value); + } + + public void write(byte[] value, String name, byte[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BYTE_1D); + write(value); + } + + public void write(byte[][] value, String name, byte[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BYTE_2D); + write(value); + } + + public void write(int value, String name, int defVal) throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.INT); + write(value); + } + + public void write(int[] value, String name, int[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.INT_1D); + write(value); + } + + public void write(int[][] value, String name, int[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.INT_2D); + write(value); + } + + public void write(float value, String name, float defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.FLOAT); + write(value); + } + + public void write(float[] value, String name, float[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.FLOAT_1D); + write(value); + } + + public void write(float[][] value, String name, float[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.FLOAT_2D); + write(value); + } + + public void write(double value, String name, double defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.DOUBLE); + write(value); + } + + public void write(double[] value, String name, double[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.DOUBLE_1D); + write(value); + } + + public void write(double[][] value, String name, double[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.DOUBLE_2D); + write(value); + } + + public void write(long value, String name, long defVal) throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.LONG); + write(value); + } + + public void write(long[] value, String name, long[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.LONG_1D); + write(value); + } + + public void write(long[][] value, String name, long[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.LONG_2D); + write(value); + } + + public void write(short value, String name, short defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.SHORT); + write(value); + } + + public void write(short[] value, String name, short[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.SHORT_1D); + write(value); + } + + public void write(short[][] value, String name, short[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.SHORT_2D); + write(value); + } + + public void write(boolean value, String name, boolean defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BOOLEAN); + write(value); + } + + public void write(boolean[] value, String name, boolean[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BOOLEAN_1D); + write(value); + } + + public void write(boolean[][] value, String name, boolean[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BOOLEAN_2D); + write(value); + } + + public void write(String value, String name, String defVal) + throws IOException { + if (value == null ? defVal == null : value.equals(defVal)) + return; + writeAlias(name, BinaryClassField.STRING); + write(value); + } + + public void write(String[] value, String name, String[] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.STRING_1D); + write(value); + } + + public void write(String[][] value, String name, String[][] defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.STRING_2D); + write(value); + } + + public void write(BitSet value, String name, BitSet defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BITSET); + write(value); + } + + public void write(Savable object, String name, Savable defVal) + throws IOException { + if (object == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE); + write(object); + } + + public void write(Savable[] objects, String name, Savable[] defVal) + throws IOException { + if (objects == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE_1D); + write(objects); + } + + public void write(Savable[][] objects, String name, Savable[][] defVal) + throws IOException { + if (objects == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE_2D); + write(objects); + } + + public void write(FloatBuffer value, String name, FloatBuffer defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.FLOATBUFFER); + write(value); + } + + public void write(IntBuffer value, String name, IntBuffer defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.INTBUFFER); + write(value); + } + + public void write(ByteBuffer value, String name, ByteBuffer defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.BYTEBUFFER); + write(value); + } + + public void write(ShortBuffer value, String name, ShortBuffer defVal) + throws IOException { + if (value == defVal) + return; + writeAlias(name, BinaryClassField.SHORTBUFFER); + write(value); + } + + public void writeFloatBufferArrayList(ArrayList<FloatBuffer> array, + String name, ArrayList<FloatBuffer> defVal) throws IOException { + if (array == defVal) + return; + writeAlias(name, BinaryClassField.FLOATBUFFER_ARRAYLIST); + writeFloatBufferArrayList(array); + } + + public void writeByteBufferArrayList(ArrayList<ByteBuffer> array, + String name, ArrayList<ByteBuffer> defVal) throws IOException { + if (array == defVal) + return; + writeAlias(name, BinaryClassField.BYTEBUFFER_ARRAYLIST); + writeByteBufferArrayList(array); + } + + public void writeSavableArrayList(ArrayList array, String name, + ArrayList defVal) throws IOException { + if (array == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE_ARRAYLIST); + writeSavableArrayList(array); + } + + public void writeSavableArrayListArray(ArrayList[] array, String name, + ArrayList[] defVal) throws IOException { + if (array == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE_ARRAYLIST_1D); + writeSavableArrayListArray(array); + } + + public void writeSavableArrayListArray2D(ArrayList[][] array, String name, + ArrayList[][] defVal) throws IOException { + if (array == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE_ARRAYLIST_2D); + writeSavableArrayListArray2D(array); + } + + public void writeSavableMap(Map<? extends Savable, ? extends Savable> map, + String name, Map<? extends Savable, ? extends Savable> defVal) + throws IOException { + if (map == defVal) + return; + writeAlias(name, BinaryClassField.SAVABLE_MAP); + writeSavableMap(map); + } + + public void writeStringSavableMap(Map<String, ? extends Savable> map, + String name, Map<String, ? extends Savable> defVal) + throws IOException { + if (map == defVal) + return; + writeAlias(name, BinaryClassField.STRING_SAVABLE_MAP); + writeStringSavableMap(map); + } + + public void writeIntSavableMap(IntMap<? extends Savable> map, + String name, IntMap<? extends Savable> defVal) + throws IOException { + if (map == defVal) + return; + writeAlias(name, BinaryClassField.INT_SAVABLE_MAP); + writeIntSavableMap(map); + } + + protected void writeAlias(String name, byte fieldType) throws IOException { + if (cObj.nameFields.get(name) == null) + generateAlias(name, fieldType); + + byte alias = cObj.nameFields.get(name).alias; + write(alias); + } + + // XXX: The generation of aliases is limited to 256 possible values. + // If we run into classes with more than 256 fields, we need to expand this. + // But I mean, come on... + protected void generateAlias(String name, byte type) { + byte alias = (byte) cObj.nameFields.size(); + cObj.nameFields.put(name, new BinaryClassField(name, alias, type)); + } + + @Override + public boolean equals(Object arg0) { + if (!(arg0 instanceof BinaryOutputCapsule)) + return false; + + byte[] other = ((BinaryOutputCapsule) arg0).bytes; + if (bytes.length != other.length) + return false; + return Arrays.equals(bytes, other); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Arrays.hashCode(this.bytes); + return hash; + } + + public void finish() { + // renamed to finish as 'finalize' in java.lang.Object should not be + // overridden like this + // - finalize should not be called directly but is called by garbage + // collection!!! + bytes = baos.toByteArray(); + baos = null; + } + + // byte primitive + + protected void write(byte value) throws IOException { + baos.write(value); + } + + protected void writeForBuffer(byte value) throws IOException { + baos.write(value); + } + + protected void write(byte[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + baos.write(value); + } + + protected void write(byte[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // int primitive + + protected void write(int value) throws IOException { + baos.write(deflate(ByteUtils.convertToBytes(value))); + } + + protected void writeForBuffer(int value) throws IOException { + byte[] byteArray = new byte[4]; + byteArray[0] = (byte) value; + byteArray[1] = (byte) (value >> 8); + byteArray[2] = (byte) (value >> 16); + byteArray[3] = (byte) (value >> 24); + baos.write(byteArray); + } + + protected void write(int[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(int[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // float primitive + + protected void write(float value) throws IOException { + baos.write(ByteUtils.convertToBytes(value)); + } + + protected void writeForBuffer(float value) throws IOException { + int integer = Float.floatToIntBits(value); + writeForBuffer(integer); + } + + protected void write(float[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(float[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // double primitive + + protected void write(double value) throws IOException { + baos.write(ByteUtils.convertToBytes(value)); + } + + protected void write(double[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(double[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // long primitive + + protected void write(long value) throws IOException { + baos.write(deflate(ByteUtils.convertToBytes(value))); + } + + protected void write(long[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(long[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // short primitive + + protected void write(short value) throws IOException { + baos.write(ByteUtils.convertToBytes(value)); + } + + protected void writeForBuffer(short value) throws IOException { + byte[] byteArray = new byte[2]; + byteArray[0] = (byte) value; + byteArray[1] = (byte) (value >> 8); + baos.write(byteArray); + } + + protected void write(short[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(short[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // boolean primitive + + protected void write(boolean value) throws IOException { + baos.write(ByteUtils.convertToBytes(value)); + } + + protected void write(boolean[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(boolean[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // String + + protected void write(String value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + // write our output as UTF-8. Java misspells UTF-8 as UTF8 for official use in java.lang + byte[] bytes = value.getBytes("UTF8"); + write(bytes.length); + baos.write(bytes); + } + + protected void write(String[] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + protected void write(String[][] value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.length); + for (int x = 0; x < value.length; x++) + write(value[x]); + } + + // BitSet + + protected void write(BitSet value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + write(value.size()); + // TODO: MAKE THIS SMALLER + for (int x = 0, max = value.size(); x < max; x++) + write(value.get(x)); + } + + // DEFLATOR for int and long + + protected static byte[] deflate(byte[] bytes) { + int size = bytes.length; + if (size == 4) { + int possibleMagic = ByteUtils.convertIntFromBytes(bytes); + if (possibleMagic == NULL_OBJECT) + return NULL_BYTES; + else if (possibleMagic == DEFAULT_OBJECT) + return DEFAULT_BYTES; + } + for (int x = 0; x < bytes.length; x++) { + if (bytes[x] != 0) + break; + size--; + } + if (size == 0) + return new byte[1]; + + byte[] rVal = new byte[1 + size]; + rVal[0] = (byte) size; + for (int x = 1; x < rVal.length; x++) + rVal[x] = bytes[bytes.length - size - 1 + x]; + + return rVal; + } + + // BinarySavable + + protected void write(Savable object) throws IOException { + if (object == null) { + write(NULL_OBJECT); + return; + } + int id = exporter.processBinarySavable(object); + write(id); + } + + // BinarySavable array + + protected void write(Savable[] objects) throws IOException { + if (objects == null) { + write(NULL_OBJECT); + return; + } + write(objects.length); + for (int x = 0; x < objects.length; x++) { + write(objects[x]); + } + } + + protected void write(Savable[][] objects) throws IOException { + if (objects == null) { + write(NULL_OBJECT); + return; + } + write(objects.length); + for (int x = 0; x < objects.length; x++) { + write(objects[x]); + } + } + + // ArrayList<BinarySavable> + + protected void writeSavableArrayList(ArrayList array) throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.size()); + for (Object bs : array) { + write((Savable) bs); + } + } + + protected void writeSavableArrayListArray(ArrayList[] array) + throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.length); + for (ArrayList bs : array) { + writeSavableArrayList(bs); + } + } + + protected void writeSavableArrayListArray2D(ArrayList[][] array) + throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.length); + for (ArrayList[] bs : array) { + writeSavableArrayListArray(bs); + } + } + + // Map<BinarySavable, BinarySavable> + + protected void writeSavableMap( + Map<? extends Savable, ? extends Savable> array) throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.size()); + for (Savable key : array.keySet()) { + write(new Savable[] { key, array.get(key) }); + } + } + + protected void writeStringSavableMap(Map<String, ? extends Savable> array) + throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.size()); + + // write String array for keys + String[] keys = array.keySet().toArray(new String[] {}); + write(keys); + + // write Savable array for values + Savable[] values = array.values().toArray(new Savable[] {}); + write(values); + } + + protected void writeIntSavableMap(IntMap<? extends Savable> array) + throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.size()); + + int[] keys = new int[array.size()]; + Savable[] values = new Savable[keys.length]; + int i = 0; + for (Entry<? extends Savable> entry : array){ + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + i++; + } + + // write String array for keys + write(keys); + + // write Savable array for values + write(values); + } + + // ArrayList<FloatBuffer> + + protected void writeFloatBufferArrayList(ArrayList<FloatBuffer> array) + throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.size()); + for (FloatBuffer buf : array) { + write(buf); + } + } + + // ArrayList<FloatBuffer> + + protected void writeByteBufferArrayList(ArrayList<ByteBuffer> array) + throws IOException { + if (array == null) { + write(NULL_OBJECT); + return; + } + write(array.size()); + for (ByteBuffer buf : array) { + write(buf); + } + } + + // NIO BUFFERS + // float buffer + + protected void write(FloatBuffer value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + value.rewind(); + int length = value.limit(); + write(length); + for (int x = 0; x < length; x++) { + writeForBuffer(value.get()); + } + value.rewind(); + } + + // int buffer + + protected void write(IntBuffer value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + value.rewind(); + int length = value.limit(); + write(length); + + for (int x = 0; x < length; x++) { + writeForBuffer(value.get()); + } + value.rewind(); + } + + // byte buffer + + protected void write(ByteBuffer value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + value.rewind(); + int length = value.limit(); + write(length); + for (int x = 0; x < length; x++) { + writeForBuffer(value.get()); + } + value.rewind(); + } + + // short buffer + + protected void write(ShortBuffer value) throws IOException { + if (value == null) { + write(NULL_OBJECT); + return; + } + value.rewind(); + int length = value.limit(); + write(length); + for (int x = 0; x < length; x++) { + writeForBuffer(value.get()); + } + value.rewind(); + } + + public void write(Enum value, String name, Enum defVal) throws IOException { + if (value == defVal) + return; + if (value == null) { + return; + } else { + write(value.name(), name, null); + } + } +}
\ No newline at end of file diff --git a/engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java b/engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java new file mode 100644 index 0000000..bb835d0 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2009-2010 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.export.binary; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * <code>ByteUtils</code> is a helper class for converting numeric primitives + * to and from byte representations. + * + * @author Joshua Slack + */ +public class ByteUtils { + + /** + * Takes an InputStream and returns the complete byte content of it + * + * @param inputStream + * The input stream to read from + * @return The byte array containing the data from the input stream + * @throws java.io.IOException + * thrown if there is a problem reading from the input stream + * provided + */ + public static byte[] getByteContent(InputStream inputStream) + throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream( + 16 * 1024); + byte[] buffer = new byte[1024]; + int byteCount = -1; + byte[] data = null; + + // Read the byte content into the output stream first + while ((byteCount = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, byteCount); + } + + // Set data with byte content from stream + data = outputStream.toByteArray(); + + // Release resources + outputStream.close(); + + return data; + } + + + // ********** byte <> short METHODS ********** + + /** + * Writes a short out to an OutputStream. + * + * @param outputStream + * The OutputStream the short will be written to + * @param value + * The short to write + * @throws IOException + * Thrown if there is a problem writing to the OutputStream + */ + public static void writeShort(OutputStream outputStream, short value) + throws IOException { + byte[] byteArray = convertToBytes(value); + + outputStream.write(byteArray); + + return; + } + + public static byte[] convertToBytes(short value) { + byte[] byteArray = new byte[2]; + + byteArray[0] = (byte) (value >> 8); + byteArray[1] = (byte) value; + return byteArray; + } + + /** + * Read in a short from an InputStream + * + * @param inputStream + * The InputStream used to read the short + * @return A short, which is the next 2 bytes converted from the InputStream + * @throws IOException + * Thrown if there is a problem reading from the InputStream + */ + public static short readShort(InputStream inputStream) throws IOException { + byte[] byteArray = new byte[2]; + + // Read in the next 2 bytes + inputStream.read(byteArray); + + short number = convertShortFromBytes(byteArray); + + return number; + } + + public static short convertShortFromBytes(byte[] byteArray) { + return convertShortFromBytes(byteArray, 0); + } + + public static short convertShortFromBytes(byte[] byteArray, int offset) { + // Convert it to a short + short number = (short) ((byteArray[offset+1] & 0xFF) + ((byteArray[offset+0] & 0xFF) << 8)); + return number; + } + + + // ********** byte <> int METHODS ********** + + /** + * Writes an integer out to an OutputStream. + * + * @param outputStream + * The OutputStream the integer will be written to + * @param integer + * The integer to write + * @throws IOException + * Thrown if there is a problem writing to the OutputStream + */ + public static void writeInt(OutputStream outputStream, int integer) + throws IOException { + byte[] byteArray = convertToBytes(integer); + + outputStream.write(byteArray); + + return; + } + + public static byte[] convertToBytes(int integer) { + byte[] byteArray = new byte[4]; + + byteArray[0] = (byte) (integer >> 24); + byteArray[1] = (byte) (integer >> 16); + byteArray[2] = (byte) (integer >> 8); + byteArray[3] = (byte) integer; + return byteArray; + } + + /** + * Read in an integer from an InputStream + * + * @param inputStream + * The InputStream used to read the integer + * @return An int, which is the next 4 bytes converted from the InputStream + * @throws IOException + * Thrown if there is a problem reading from the InputStream + */ + public static int readInt(InputStream inputStream) throws IOException { + byte[] byteArray = new byte[4]; + + // Read in the next 4 bytes + inputStream.read(byteArray); + + int number = convertIntFromBytes(byteArray); + + return number; + } + + public static int convertIntFromBytes(byte[] byteArray) { + return convertIntFromBytes(byteArray, 0); + } + + public static int convertIntFromBytes(byte[] byteArray, int offset) { + // Convert it to an int + int number = ((byteArray[offset] & 0xFF) << 24) + + ((byteArray[offset+1] & 0xFF) << 16) + ((byteArray[offset+2] & 0xFF) << 8) + + (byteArray[offset+3] & 0xFF); + return number; + } + + + // ********** byte <> long METHODS ********** + + /** + * Writes a long out to an OutputStream. + * + * @param outputStream + * The OutputStream the long will be written to + * @param value + * The long to write + * @throws IOException + * Thrown if there is a problem writing to the OutputStream + */ + public static void writeLong(OutputStream outputStream, long value) + throws IOException { + byte[] byteArray = convertToBytes(value); + + outputStream.write(byteArray); + + return; + } + + public static byte[] convertToBytes(long n) { + byte[] bytes = new byte[8]; + + bytes[7] = (byte) (n); + n >>>= 8; + bytes[6] = (byte) (n); + n >>>= 8; + bytes[5] = (byte) (n); + n >>>= 8; + bytes[4] = (byte) (n); + n >>>= 8; + bytes[3] = (byte) (n); + n >>>= 8; + bytes[2] = (byte) (n); + n >>>= 8; + bytes[1] = (byte) (n); + n >>>= 8; + bytes[0] = (byte) (n); + + return bytes; + } + + /** + * Read in a long from an InputStream + * + * @param inputStream + * The InputStream used to read the long + * @return A long, which is the next 8 bytes converted from the InputStream + * @throws IOException + * Thrown if there is a problem reading from the InputStream + */ + public static long readLong(InputStream inputStream) throws IOException { + byte[] byteArray = new byte[8]; + + // Read in the next 8 bytes + inputStream.read(byteArray); + + long number = convertLongFromBytes(byteArray); + + return number; + } + + public static long convertLongFromBytes(byte[] bytes) { + return convertLongFromBytes(bytes, 0); + } + + public static long convertLongFromBytes(byte[] bytes, int offset) { + // Convert it to an long + return ((((long) bytes[offset+7]) & 0xFF) + + ((((long) bytes[offset+6]) & 0xFF) << 8) + + ((((long) bytes[offset+5]) & 0xFF) << 16) + + ((((long) bytes[offset+4]) & 0xFF) << 24) + + ((((long) bytes[offset+3]) & 0xFF) << 32) + + ((((long) bytes[offset+2]) & 0xFF) << 40) + + ((((long) bytes[offset+1]) & 0xFF) << 48) + + ((((long) bytes[offset+0]) & 0xFF) << 56)); + } + + + // ********** byte <> double METHODS ********** + + /** + * Writes a double out to an OutputStream. + * + * @param outputStream + * The OutputStream the double will be written to + * @param value + * The double to write + * @throws IOException + * Thrown if there is a problem writing to the OutputStream + */ + public static void writeDouble(OutputStream outputStream, double value) + throws IOException { + byte[] byteArray = convertToBytes(value); + + outputStream.write(byteArray); + + return; + } + + public static byte[] convertToBytes(double n) { + long bits = Double.doubleToLongBits(n); + return convertToBytes(bits); + } + + /** + * Read in a double from an InputStream + * + * @param inputStream + * The InputStream used to read the double + * @return A double, which is the next 8 bytes converted from the InputStream + * @throws IOException + * Thrown if there is a problem reading from the InputStream + */ + public static double readDouble(InputStream inputStream) throws IOException { + byte[] byteArray = new byte[8]; + + // Read in the next 8 bytes + inputStream.read(byteArray); + + double number = convertDoubleFromBytes(byteArray); + + return number; + } + + public static double convertDoubleFromBytes(byte[] bytes) { + return convertDoubleFromBytes(bytes, 0); + } + + public static double convertDoubleFromBytes(byte[] bytes, int offset) { + // Convert it to a double + long bits = convertLongFromBytes(bytes, offset); + return Double.longBitsToDouble(bits); + } + + // ********** byte <> float METHODS ********** + + /** + * Writes an float out to an OutputStream. + * + * @param outputStream + * The OutputStream the float will be written to + * @param fVal + * The float to write + * @throws IOException + * Thrown if there is a problem writing to the OutputStream + */ + public static void writeFloat(OutputStream outputStream, float fVal) + throws IOException { + byte[] byteArray = convertToBytes(fVal); + + outputStream.write(byteArray); + + return; + } + + public static byte[] convertToBytes(float f) { + int temp = Float.floatToIntBits(f); + return convertToBytes(temp); + } + + /** + * Read in a float from an InputStream + * + * @param inputStream + * The InputStream used to read the float + * @return A float, which is the next 4 bytes converted from the InputStream + * @throws IOException + * Thrown if there is a problem reading from the InputStream + */ + public static float readFloat(InputStream inputStream) throws IOException { + byte[] byteArray = new byte[4]; + + // Read in the next 4 bytes + inputStream.read(byteArray); + + float number = convertFloatFromBytes(byteArray); + + return number; + } + + public static float convertFloatFromBytes(byte[] byteArray) { + return convertFloatFromBytes(byteArray, 0); + } + public static float convertFloatFromBytes(byte[] byteArray, int offset) { + // Convert it to an int + int number = convertIntFromBytes(byteArray, offset); + return Float.intBitsToFloat(number); + } + + + + // ********** byte <> boolean METHODS ********** + + /** + * Writes a boolean out to an OutputStream. + * + * @param outputStream + * The OutputStream the boolean will be written to + * @param bVal + * The boolean to write + * @throws IOException + * Thrown if there is a problem writing to the OutputStream + */ + public static void writeBoolean(OutputStream outputStream, boolean bVal) + throws IOException { + byte[] byteArray = convertToBytes(bVal); + + outputStream.write(byteArray); + + return; + } + + public static byte[] convertToBytes(boolean b) { + byte[] rVal = new byte[1]; + rVal[0] = b ? (byte)1 : (byte)0; + return rVal; + } + + /** + * Read in a boolean from an InputStream + * + * @param inputStream + * The InputStream used to read the boolean + * @return A boolean, which is the next byte converted from the InputStream (iow, byte != 0) + * @throws IOException + * Thrown if there is a problem reading from the InputStream + */ + public static boolean readBoolean(InputStream inputStream) throws IOException { + byte[] byteArray = new byte[1]; + + // Read in the next byte + inputStream.read(byteArray); + + return convertBooleanFromBytes(byteArray); + } + + public static boolean convertBooleanFromBytes(byte[] byteArray) { + return convertBooleanFromBytes(byteArray, 0); + } + public static boolean convertBooleanFromBytes(byte[] byteArray, int offset) { + return byteArray[offset] != 0; + } + + + /** + * Properly reads in data from the given stream until the specified number + * of bytes have been read. + * + * @param store + * the byte array to store in. Should have a length > bytes + * @param bytes + * the number of bytes to read. + * @param is + * the stream to read from + * @return the store array for chaining purposes + * @throws IOException + * if an error occurs while reading from the stream + * @throws ArrayIndexOutOfBoundsException + * if bytes greater than the length of the store. + */ + public static byte[] readData(byte[] store, int bytes, InputStream is) throws IOException { + for (int i = 0; i < bytes; i++) { + store[i] = (byte)is.read(); + } + return store; + } + + public static byte[] rightAlignBytes(byte[] bytes, int width) { + if (bytes.length != width) { + byte[] rVal = new byte[width]; + for (int x = width - bytes.length; x < width; x++) { + rVal[x] = bytes[x - (width - bytes.length)]; + } + return rVal; + } + + return bytes; + } + +} diff --git a/engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java b/engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java new file mode 100644 index 0000000..688627d --- /dev/null +++ b/engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2009-2010 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.font.plugins; + +import com.jme3.asset.*; +import com.jme3.font.BitmapCharacter; +import com.jme3.font.BitmapCharacterSet; +import com.jme3.font.BitmapFont; +import com.jme3.material.Material; +import com.jme3.material.MaterialDef; +import com.jme3.material.RenderState.BlendMode; +import com.jme3.texture.Texture; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public class BitmapFontLoader implements AssetLoader { + + private BitmapFont load(AssetManager assetManager, String folder, InputStream in) throws IOException{ + MaterialDef spriteMat = + (MaterialDef) assetManager.loadAsset(new AssetKey("Common/MatDefs/Misc/Unshaded.j3md")); + + BitmapCharacterSet charSet = new BitmapCharacterSet(); + Material[] matPages = null; + BitmapFont font = new BitmapFont(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String regex = "[\\s=]+"; + + font.setCharSet(charSet); + while (reader.ready()){ + String line = reader.readLine(); + String[] tokens = line.split(regex); + if (tokens[0].equals("info")){ + // Get rendered size + for (int i = 1; i < tokens.length; i++){ + if (tokens[i].equals("size")){ + charSet.setRenderedSize(Integer.parseInt(tokens[i + 1])); + } + } + }else if (tokens[0].equals("common")){ + // Fill out BitmapCharacterSet fields + for (int i = 1; i < tokens.length; i++){ + String token = tokens[i]; + if (token.equals("lineHeight")){ + charSet.setLineHeight(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("base")){ + charSet.setBase(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("scaleW")){ + charSet.setWidth(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("scaleH")){ + charSet.setHeight(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("pages")){ + // number of texture pages + matPages = new Material[Integer.parseInt(tokens[i + 1])]; + font.setPages(matPages); + } + } + }else if (tokens[0].equals("page")){ + int index = -1; + Texture tex = null; + + for (int i = 1; i < tokens.length; i++){ + String token = tokens[i]; + if (token.equals("id")){ + index = Integer.parseInt(tokens[i + 1]); + }else if (token.equals("file")){ + String file = tokens[i + 1]; + if (file.startsWith("\"")){ + file = file.substring(1, file.length()-1); + } + TextureKey key = new TextureKey(folder + file, true); + key.setGenerateMips(false); + tex = assetManager.loadTexture(key); + tex.setMagFilter(Texture.MagFilter.Bilinear); + tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + } + } + // set page + if (index >= 0 && tex != null){ + Material mat = new Material(spriteMat); + mat.setTexture("ColorMap", tex); + mat.setBoolean("VertexColor", true); + mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); + matPages[index] = mat; + } + }else if (tokens[0].equals("char")){ + // New BitmapCharacter + BitmapCharacter ch = null; + for (int i = 1; i < tokens.length; i++){ + String token = tokens[i]; + if (token.equals("id")){ + int index = Integer.parseInt(tokens[i + 1]); + ch = new BitmapCharacter(); + charSet.addCharacter(index, ch); + }else if (token.equals("x")){ + ch.setX(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("y")){ + ch.setY(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("width")){ + ch.setWidth(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("height")){ + ch.setHeight(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("xoffset")){ + ch.setXOffset(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("yoffset")){ + ch.setYOffset(Integer.parseInt(tokens[i + 1])); + }else if (token.equals("xadvance")){ + ch.setXAdvance(Integer.parseInt(tokens[i + 1])); + } else if (token.equals("page")) { + ch.setPage(Integer.parseInt(tokens[i + 1])); + } + } + }else if (tokens[0].equals("kerning")){ + // Build kerning list + int index = 0; + int second = 0; + int amount = 0; + + for (int i = 1; i < tokens.length; i++){ + if (tokens[i].equals("first")){ + index = Integer.parseInt(tokens[i + 1]); + }else if (tokens[i].equals("second")){ + second = Integer.parseInt(tokens[i + 1]); + }else if (tokens[i].equals("amount")){ + amount = Integer.parseInt(tokens[i + 1]); + } + } + + BitmapCharacter ch = charSet.getCharacter(index); + ch.addKerning(second, amount); + } + } + return font; + } + + public Object load(AssetInfo info) throws IOException { + InputStream in = null; + try { + in = info.openStream(); + BitmapFont font = load(info.getManager(), info.getKey().getFolder(), in); + return font; + } finally { + if (in != null){ + in.close(); + } + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java b/engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java new file mode 100644 index 0000000..e07d3e4 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2009-2010 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.material.plugins; + +import com.jme3.asset.*; +import com.jme3.material.RenderState.BlendMode; +import com.jme3.material.RenderState.FaceCullMode; +import com.jme3.material.*; +import com.jme3.material.TechniqueDef.LightMode; +import com.jme3.material.TechniqueDef.ShadowMode; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.shader.VarType; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.Texture2D; +import com.jme3.util.PlaceholderAssets; +import com.jme3.util.blockparser.BlockLanguageParser; +import com.jme3.util.blockparser.Statement; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class J3MLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(J3MLoader.class.getName()); + + private AssetManager assetManager; + private AssetKey key; + + private MaterialDef materialDef; + private Material material; + private TechniqueDef technique; + private RenderState renderState; + + private String shaderLang; + private String vertName; + private String fragName; + + private static final String whitespacePattern = "\\p{javaWhitespace}+"; + + public J3MLoader(){ + } + + private void throwIfNequal(String expected, String got) throws IOException { + if (expected == null) + throw new IOException("Expected a statement, got '"+got+"'!"); + + if (!expected.equals(got)) + throw new IOException("Expected '"+expected+"', got '"+got+"'!"); + } + + // <TYPE> <LANG> : <SOURCE> + private void readShaderStatement(String statement) throws IOException { + String[] split = statement.split(":"); + if (split.length != 2){ + throw new IOException("Shader statement syntax incorrect" + statement); + } + String[] typeAndLang = split[0].split(whitespacePattern); + if (typeAndLang.length != 2){ + throw new IOException("Shader statement syntax incorrect: " + statement); + } + shaderLang = typeAndLang[1]; + if (typeAndLang[0].equals("VertexShader")){ + vertName = split[1].trim(); + }else if (typeAndLang[0].equals("FragmentShader")){ + fragName = split[1].trim(); + } + } + + // LightMode <MODE> + private void readLightMode(String statement) throws IOException{ + String[] split = statement.split(whitespacePattern); + if (split.length != 2){ + throw new IOException("LightMode statement syntax incorrect"); + } + LightMode lm = LightMode.valueOf(split[1]); + technique.setLightMode(lm); + } + + // ShadowMode <MODE> + private void readShadowMode(String statement) throws IOException{ + String[] split = statement.split(whitespacePattern); + if (split.length != 2){ + throw new IOException("ShadowMode statement syntax incorrect"); + } + ShadowMode sm = ShadowMode.valueOf(split[1]); + technique.setShadowMode(sm); + } + + private Object readValue(VarType type, String value) throws IOException{ + if (type.isTextureType()){ +// String texturePath = readString("[\n;(//)(\\})]"); + String texturePath = value.trim(); + boolean flipY = false; + boolean repeat = false; + if (texturePath.startsWith("Flip Repeat ")){ + texturePath = texturePath.substring(12).trim(); + flipY = true; + repeat = true; + }else if (texturePath.startsWith("Flip ")){ + texturePath = texturePath.substring(5).trim(); + flipY = true; + }else if (texturePath.startsWith("Repeat ")){ + texturePath = texturePath.substring(7).trim(); + repeat = true; + } + + TextureKey texKey = new TextureKey(texturePath, flipY); + texKey.setAsCube(type == VarType.TextureCubeMap); + texKey.setGenerateMips(true); + + Texture tex; + try { + tex = assetManager.loadTexture(texKey); + } catch (AssetNotFoundException ex){ + logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{texKey, key}); + tex = null; + } + if (tex != null){ + if (repeat){ + tex.setWrap(WrapMode.Repeat); + } + }else{ + tex = new Texture2D(PlaceholderAssets.getPlaceholderImage()); + } + return tex; + }else{ + String[] split = value.trim().split(whitespacePattern); + switch (type){ + case Float: + if (split.length != 1){ + throw new IOException("Float value parameter must have 1 entry: " + value); + } + return Float.parseFloat(split[0]); + case Vector2: + if (split.length != 2){ + throw new IOException("Vector2 value parameter must have 2 entries: " + value); + } + return new Vector2f(Float.parseFloat(split[0]), + Float.parseFloat(split[1])); + case Vector3: + if (split.length != 3){ + throw new IOException("Vector3 value parameter must have 3 entries: " + value); + } + return new Vector3f(Float.parseFloat(split[0]), + Float.parseFloat(split[1]), + Float.parseFloat(split[2])); + case Vector4: + if (split.length != 4){ + throw new IOException("Vector4 value parameter must have 4 entries: " + value); + } + return new ColorRGBA(Float.parseFloat(split[0]), + Float.parseFloat(split[1]), + Float.parseFloat(split[2]), + Float.parseFloat(split[3])); + case Int: + if (split.length != 1){ + throw new IOException("Int value parameter must have 1 entry: " + value); + } + return Integer.parseInt(split[0]); + case Boolean: + if (split.length != 1){ + throw new IOException("Boolean value parameter must have 1 entry: " + value); + } + return Boolean.parseBoolean(split[0]); + default: + throw new UnsupportedOperationException("Unknown type: "+type); + } + } + } + + // <TYPE> <NAME> [ "(" <FFBINDING> ")" ] [ ":" <DEFAULTVAL> ] + private void readParam(String statement) throws IOException{ + String name; + String defaultVal = null; + FixedFuncBinding ffBinding = null; + + String[] split = statement.split(":"); + + // Parse default val + if (split.length == 1){ + // Doesn't contain default value + }else{ + if (split.length != 2){ + throw new IOException("Parameter statement syntax incorrect"); + } + statement = split[0].trim(); + defaultVal = split[1].trim(); + } + + // Parse ffbinding + int startParen = statement.indexOf("("); + if (startParen != -1){ + // get content inside parentheses + int endParen = statement.indexOf(")", startParen); + String bindingStr = statement.substring(startParen+1, endParen).trim(); + try { + ffBinding = FixedFuncBinding.valueOf(bindingStr); + } catch (IllegalArgumentException ex){ + throw new IOException("FixedFuncBinding '" + + split[1] + "' does not exist!"); + } + statement = statement.substring(0, startParen); + } + + // Parse type + name + split = statement.split(whitespacePattern); + if (split.length != 2){ + throw new IOException("Parameter statement syntax incorrect"); + } + + VarType type; + if (split[0].equals("Color")){ + type = VarType.Vector4; + }else{ + type = VarType.valueOf(split[0]); + } + + name = split[1]; + + Object defaultValObj = null; + if (defaultVal != null){ + defaultValObj = readValue(type, defaultVal); + } + + materialDef.addMaterialParam(type, name, defaultValObj, ffBinding); + } + + private void readValueParam(String statement) throws IOException{ + // Use limit=1 incase filename contains colons + String[] split = statement.split(":", 2); + if (split.length != 2){ + throw new IOException("Value parameter statement syntax incorrect"); + } + String name = split[0].trim(); + + // parse value + MatParam p = material.getMaterialDef().getMaterialParam(name); + if (p == null){ + throw new IOException("The material parameter: "+name+" is undefined."); + } + + Object valueObj = readValue(p.getVarType(), split[1]); + if (p.getVarType().isTextureType()){ + material.setTextureParam(name, p.getVarType(), (Texture) valueObj); + }else{ + material.setParam(name, p.getVarType(), valueObj); + } + } + + private void readMaterialParams(List<Statement> paramsList) throws IOException{ + for (Statement statement : paramsList){ + readParam(statement.getLine()); + } + } + + private void readExtendingMaterialParams(List<Statement> paramsList) throws IOException{ + for (Statement statement : paramsList){ + readValueParam(statement.getLine()); + } + } + + private void readWorldParams(List<Statement> worldParams) throws IOException{ + for (Statement statement : worldParams){ + technique.addWorldParam(statement.getLine()); + } + } + + private boolean parseBoolean(String word){ + return word != null && word.equals("On"); + } + + private void readRenderStateStatement(String statement) throws IOException{ + String[] split = statement.split(whitespacePattern); + if (split[0].equals("Wireframe")){ + renderState.setWireframe(parseBoolean(split[1])); + }else if (split[0].equals("FaceCull")){ + renderState.setFaceCullMode(FaceCullMode.valueOf(split[1])); + }else if (split[0].equals("DepthWrite")){ + renderState.setDepthWrite(parseBoolean(split[1])); + }else if (split[0].equals("DepthTest")){ + renderState.setDepthTest(parseBoolean(split[1])); + }else if (split[0].equals("Blend")){ + renderState.setBlendMode(BlendMode.valueOf(split[1])); + }else if (split[0].equals("AlphaTestFalloff")){ + renderState.setAlphaTest(true); + renderState.setAlphaFallOff(Float.parseFloat(split[1])); + }else if (split[0].equals("PolyOffset")){ + float factor = Float.parseFloat(split[1]); + float units = Float.parseFloat(split[2]); + renderState.setPolyOffset(factor, units); + }else if (split[0].equals("ColorWrite")){ + renderState.setColorWrite(parseBoolean(split[1])); + }else if (split[0].equals("PointSprite")){ + renderState.setPointSprite(parseBoolean(split[1])); + }else{ + throwIfNequal(null, split[0]); + } + } + + private void readAdditionalRenderState(List<Statement> renderStates) throws IOException{ + renderState = material.getAdditionalRenderState(); + for (Statement statement : renderStates){ + readRenderStateStatement(statement.getLine()); + } + renderState = null; + } + + private void readRenderState(List<Statement> renderStates) throws IOException{ + renderState = new RenderState(); + for (Statement statement : renderStates){ + readRenderStateStatement(statement.getLine()); + } + technique.setRenderState(renderState); + renderState = null; + } + + // <DEFINENAME> [ ":" <PARAMNAME> ] + private void readDefine(String statement) throws IOException{ + String[] split = statement.split(":"); + if (split.length == 1){ + // add preset define + technique.addShaderPresetDefine(split[0].trim(), VarType.Boolean, true); + }else if (split.length == 2){ + technique.addShaderParamDefine(split[1].trim(), split[0].trim()); + }else{ + throw new IOException("Define syntax incorrect"); + } + } + + private void readDefines(List<Statement> defineList) throws IOException{ + for (Statement statement : defineList){ + readDefine(statement.getLine()); + } + + } + + private void readTechniqueStatement(Statement statement) throws IOException{ + String[] split = statement.getLine().split("[ \\{]"); + if (split[0].equals("VertexShader") || + split[0].equals("FragmentShader")){ + readShaderStatement(statement.getLine()); + }else if (split[0].equals("LightMode")){ + readLightMode(statement.getLine()); + }else if (split[0].equals("ShadowMode")){ + readShadowMode(statement.getLine()); + }else if (split[0].equals("WorldParameters")){ + readWorldParams(statement.getContents()); + }else if (split[0].equals("RenderState")){ + readRenderState(statement.getContents()); + }else if (split[0].equals("Defines")){ + readDefines(statement.getContents()); + }else{ + throwIfNequal(null, split[0]); + } + } + + private void readTransparentStatement(String statement) throws IOException{ + String[] split = statement.split(whitespacePattern); + if (split.length != 2){ + throw new IOException("Transparent statement syntax incorrect"); + } + material.setTransparent(parseBoolean(split[1])); + } + + private void readTechnique(Statement techStat) throws IOException{ + String[] split = techStat.getLine().split(whitespacePattern); + if (split.length == 1){ + technique = new TechniqueDef(null); + }else if (split.length == 2){ + technique = new TechniqueDef(split[1]); + }else{ + throw new IOException("Technique statement syntax incorrect"); + } + + for (Statement statement : techStat.getContents()){ + readTechniqueStatement(statement); + } + + if (vertName != null && fragName != null){ + technique.setShaderFile(vertName, fragName, shaderLang); + } + + materialDef.addTechniqueDef(technique); + technique = null; + vertName = null; + fragName = null; + shaderLang = null; + } + + private void loadFromRoot(List<Statement> roots) throws IOException{ + if (roots.size() == 2){ + Statement exception = roots.get(0); + String line = exception.getLine(); + if (line.startsWith("Exception")){ + throw new AssetLoadException(line.substring("Exception ".length())); + }else{ + throw new IOException("In multiroot material, expected first statement to be 'Exception'"); + } + }else if (roots.size() != 1){ + throw new IOException("Too many roots in J3M/J3MD file"); + } + + boolean extending = false; + Statement materialStat = roots.get(0); + String materialName = materialStat.getLine(); + if (materialName.startsWith("MaterialDef")){ + materialName = materialName.substring("MaterialDef ".length()).trim(); + extending = false; + }else if (materialName.startsWith("Material")){ + materialName = materialName.substring("Material ".length()).trim(); + extending = true; + }else{ + throw new IOException("Specified file is not a Material file"); + } + + String[] split = materialName.split(":", 2); + + if (materialName.equals("")){ + throw new IOException("Material name cannot be empty"); + } + + if (split.length == 2){ + if (!extending){ + throw new IOException("Must use 'Material' when extending."); + } + + String extendedMat = split[1].trim(); + + MaterialDef def = (MaterialDef) assetManager.loadAsset(new AssetKey(extendedMat)); + if (def == null) + throw new IOException("Extended material "+extendedMat+" cannot be found."); + + material = new Material(def); + material.setKey(key); +// material.setAssetName(fileName); + }else if (split.length == 1){ + if (extending){ + throw new IOException("Expected ':', got '{'"); + } + materialDef = new MaterialDef(assetManager, materialName); + // NOTE: pass file name for defs so they can be loaded later + materialDef.setAssetName(key.getName()); + }else{ + throw new IOException("Cannot use colon in material name/path"); + } + + for (Statement statement : materialStat.getContents()){ + split = statement.getLine().split("[ \\{]"); + String statType = split[0]; + if (extending){ + if (statType.equals("MaterialParameters")){ + readExtendingMaterialParams(statement.getContents()); + }else if (statType.equals("AdditionalRenderState")){ + readAdditionalRenderState(statement.getContents()); + }else if (statType.equals("Transparent")){ + readTransparentStatement(statement.getLine()); + } + }else{ + if (statType.equals("Technique")){ + readTechnique(statement); + }else if (statType.equals("MaterialParameters")){ + readMaterialParams(statement.getContents()); + }else{ + throw new IOException("Expected material statement, got '"+statType+"'"); + } + } + } + } + + public Object load(AssetInfo info) throws IOException { + this.assetManager = info.getManager(); + + InputStream in = info.openStream(); + try { + key = info.getKey(); + loadFromRoot(BlockLanguageParser.parse(in)); + } finally { + if (in != null){ + in.close(); + } + } + + if (material != null){ + if (!(info.getKey() instanceof MaterialKey)){ + throw new IOException("Material instances must be loaded via MaterialKey"); + } + // material implementation + return material; + }else{ + // material definition + return materialDef; + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java b/engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java new file mode 100644 index 0000000..1f701cd --- /dev/null +++ b/engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2009-2010 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.scene.plugins; + +import com.jme3.asset.*; +import com.jme3.material.Material; +import com.jme3.material.MaterialList; +import com.jme3.material.RenderState.BlendMode; +import com.jme3.math.ColorRGBA; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.Texture2D; +import com.jme3.util.PlaceholderAssets; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MTLLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(MTLLoader.class.getName()); + + protected Scanner scan; + protected MaterialList matList; + //protected Material material; + protected AssetManager assetManager; + protected String folderName; + protected AssetKey key; + + protected Texture diffuseMap, normalMap, specularMap, alphaMap; + protected ColorRGBA ambient = new ColorRGBA(); + protected ColorRGBA diffuse = new ColorRGBA(); + protected ColorRGBA specular = new ColorRGBA(); + protected float shininess = 16; + protected boolean shadeless; + protected String matName; + protected float alpha = 1; + protected boolean transparent = false; + protected boolean disallowTransparency = false; + protected boolean disallowAmbient = false; + protected boolean disallowSpecular = false; + + public void reset(){ + scan = null; + matList = null; +// material = null; + + resetMaterial(); + } + + protected ColorRGBA readColor(){ + ColorRGBA v = new ColorRGBA(); + v.set(scan.nextFloat(), scan.nextFloat(), scan.nextFloat(), 1.0f); + return v; + } + + protected String nextStatement(){ + scan.useDelimiter("\n"); + String result = scan.next(); + scan.useDelimiter("\\p{javaWhitespace}+"); + return result; + } + + protected boolean skipLine(){ + try { + scan.skip(".*\r{0,1}\n"); + return true; + } catch (NoSuchElementException ex){ + // EOF + return false; + } + } + + protected void resetMaterial(){ + ambient.set(ColorRGBA.DarkGray); + diffuse.set(ColorRGBA.LightGray); + specular.set(ColorRGBA.Black); + shininess = 16; + disallowTransparency = false; + disallowAmbient = false; + disallowSpecular = false; + shadeless = false; + transparent = false; + matName = null; + diffuseMap = null; + specularMap = null; + normalMap = null; + alphaMap = null; + alpha = 1; + } + + protected void createMaterial(){ + Material material; + + if (alpha < 1f && transparent && !disallowTransparency){ + diffuse.a = alpha; + } + + if (shadeless){ + material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", diffuse.clone()); + material.setTexture("ColorMap", diffuseMap); + // TODO: Add handling for alpha map? + }else{ + material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + material.setBoolean("UseMaterialColors", true); + material.setColor("Ambient", ambient.clone()); + material.setColor("Diffuse", diffuse.clone()); + material.setColor("Specular", specular.clone()); + material.setFloat("Shininess", shininess); // prevents "premature culling" bug + + if (diffuseMap != null) material.setTexture("DiffuseMap", diffuseMap); + if (specularMap != null) material.setTexture("SpecularMap", specularMap); + if (normalMap != null) material.setTexture("NormalMap", normalMap); + if (alphaMap != null) material.setTexture("AlphaMap", alphaMap); + } + + if (transparent && !disallowTransparency){ + material.setTransparent(true); + material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); + material.getAdditionalRenderState().setAlphaTest(true); + material.getAdditionalRenderState().setAlphaFallOff(0.01f); + } + + matList.put(matName, material); + } + + protected void startMaterial(String name){ + if (matName != null){ + // material is already in cache, generate it + createMaterial(); + } + + // now, reset the params and set the name to start a new material + resetMaterial(); + matName = name; + } + + protected Texture loadTexture(String path){ + String[] split = path.trim().split("\\p{javaWhitespace}+"); + + // will crash if path is an empty string + path = split[split.length-1]; + + String name = new File(path).getName(); + TextureKey texKey = new TextureKey(folderName + name); + texKey.setGenerateMips(true); + Texture texture; + try { + texture = assetManager.loadTexture(texKey); + texture.setWrap(WrapMode.Repeat); + } catch (AssetNotFoundException ex){ + logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{texKey, key}); + texture = new Texture2D(PlaceholderAssets.getPlaceholderImage()); + } + return texture; + } + + protected boolean readLine(){ + if (!scan.hasNext()){ + return false; + } + + String cmd = scan.next().toLowerCase(); + if (cmd.startsWith("#")){ + // skip entire comment until next line + return skipLine(); + }else if (cmd.equals("newmtl")){ + String name = scan.next(); + startMaterial(name); + }else if (cmd.equals("ka")){ + ambient.set(readColor()); + }else if (cmd.equals("kd")){ + diffuse.set(readColor()); + }else if (cmd.equals("ks")){ + specular.set(readColor()); + }else if (cmd.equals("ns")){ + float shiny = scan.nextFloat(); + if (shiny >= 1){ + shininess = shiny; /* (128f / 1000f)*/ + if (specular.equals(ColorRGBA.Black)){ + specular.set(ColorRGBA.White); + } + }else{ + // For some reason blender likes to export Ns 0 statements + // Ignore Ns 0 instead of setting it + } + + }else if (cmd.equals("d") || cmd.equals("tr")){ + alpha = scan.nextFloat(); + transparent = true; + }else if (cmd.equals("map_ka")){ + // ignore it for now + return skipLine(); + }else if (cmd.equals("map_kd")){ + String path = nextStatement(); + diffuseMap = loadTexture(path); + }else if (cmd.equals("map_bump") || cmd.equals("bump")){ + if (normalMap == null){ + String path = nextStatement(); + normalMap = loadTexture(path); + } + }else if (cmd.equals("map_ks")){ + String path = nextStatement(); + specularMap = loadTexture(path); + if (specularMap != null){ + // NOTE: since specular color is modulated with specmap + // make sure we have it set + if (specular.equals(ColorRGBA.Black)){ + specular.set(ColorRGBA.White); + } + } + }else if (cmd.equals("map_d")){ + String path = scan.next(); + alphaMap = loadTexture(path); + transparent = true; + }else if (cmd.equals("illum")){ + int mode = scan.nextInt(); + + switch (mode){ + case 0: + // no lighting + shadeless = true; + disallowTransparency = true; + break; + case 1: + disallowSpecular = true; + disallowTransparency = true; + break; + case 2: + case 3: + case 5: + case 8: + disallowTransparency = true; + break; + case 4: + case 6: + case 7: + case 9: + // Enable transparency + // Works best if diffuse map has an alpha channel + transparent = true; + break; + } + }else if (cmd.equals("ke") || cmd.equals("ni")){ + // Ni: index of refraction - unsupported in jME + // Ke: emission color + return skipLine(); + }else{ + logger.log(Level.WARNING, "Unknown statement in MTL! {0}", cmd); + return skipLine(); + } + + return true; + } + + @SuppressWarnings("empty-statement") + public Object load(AssetInfo info) throws IOException{ + reset(); + + this.key = info.getKey(); + this.assetManager = info.getManager(); + folderName = info.getKey().getFolder(); + matList = new MaterialList(); + + InputStream in = null; + try { + in = info.openStream(); + scan = new Scanner(in); + scan.useLocale(Locale.US); + + while (readLine()); + } finally { + if (in != null){ + in.close(); + } + } + + if (matName != null){ + // still have a material in the vars + createMaterial(); + resetMaterial(); + } + + MaterialList list = matList; + + + + return list; + } +} diff --git a/engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java b/engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java new file mode 100644 index 0000000..3ce7f52 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2009-2010 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.scene.plugins; + +import com.jme3.asset.*; +import com.jme3.material.Material; +import com.jme3.material.MaterialList; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.queue.RenderQueue.Bucket; +import com.jme3.scene.Mesh.Mode; +import com.jme3.scene.*; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.mesh.IndexBuffer; +import com.jme3.scene.mesh.IndexIntBuffer; +import com.jme3.scene.mesh.IndexShortBuffer; +import com.jme3.util.BufferUtils; +import com.jme3.util.IntMap; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.Map.Entry; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Reads OBJ format models. + */ +public final class OBJLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(OBJLoader.class.getName()); + + protected final ArrayList<Vector3f> verts = new ArrayList<Vector3f>(); + protected final ArrayList<Vector2f> texCoords = new ArrayList<Vector2f>(); + protected final ArrayList<Vector3f> norms = new ArrayList<Vector3f>(); + + protected final ArrayList<Face> faces = new ArrayList<Face>(); + protected final HashMap<String, ArrayList<Face>> matFaces = new HashMap<String, ArrayList<Face>>(); + + protected String currentMatName; + protected String currentObjectName; + + protected final HashMap<Vertex, Integer> vertIndexMap = new HashMap<Vertex, Integer>(100); + protected final IntMap<Vertex> indexVertMap = new IntMap<Vertex>(100); + protected int curIndex = 0; + protected int objectIndex = 0; + protected int geomIndex = 0; + + protected Scanner scan; + protected ModelKey key; + protected AssetManager assetManager; + protected MaterialList matList; + + protected String objName; + protected Node objNode; + + protected static class Vertex { + + Vector3f v; + Vector2f vt; + Vector3f vn; + int index; + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Vertex other = (Vertex) obj; + if (this.v != other.v && (this.v == null || !this.v.equals(other.v))) { + return false; + } + if (this.vt != other.vt && (this.vt == null || !this.vt.equals(other.vt))) { + return false; + } + if (this.vn != other.vn && (this.vn == null || !this.vn.equals(other.vn))) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 53 * hash + (this.v != null ? this.v.hashCode() : 0); + hash = 53 * hash + (this.vt != null ? this.vt.hashCode() : 0); + hash = 53 * hash + (this.vn != null ? this.vn.hashCode() : 0); + return hash; + } + } + + protected static class Face { + Vertex[] verticies; + } + + protected class ObjectGroup { + + final String objectName; + + public ObjectGroup(String objectName){ + this.objectName = objectName; + } + + public Spatial createGeometry(){ + Node groupNode = new Node(objectName); + +// if (matFaces.size() > 0){ +// for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){ +// ArrayList<Face> materialFaces = entry.getValue(); +// if (materialFaces.size() > 0){ +// Geometry geom = createGeometry(materialFaces, entry.getKey()); +// objNode.attachChild(geom); +// } +// } +// }else if (faces.size() > 0){ +// // generate final geometry +// Geometry geom = createGeometry(faces, null); +// objNode.attachChild(geom); +// } + + return groupNode; + } + } + + public void reset(){ + verts.clear(); + texCoords.clear(); + norms.clear(); + faces.clear(); + matFaces.clear(); + + vertIndexMap.clear(); + indexVertMap.clear(); + + currentMatName = null; + matList = null; + curIndex = 0; + geomIndex = 0; + scan = null; + } + + protected void findVertexIndex(Vertex vert){ + Integer index = vertIndexMap.get(vert); + if (index != null){ + vert.index = index.intValue(); + }else{ + vert.index = curIndex++; + vertIndexMap.put(vert, vert.index); + indexVertMap.put(vert.index, vert); + } + } + + protected Face[] quadToTriangle(Face f){ + assert f.verticies.length == 4; + + Face[] t = new Face[]{ new Face(), new Face() }; + t[0].verticies = new Vertex[3]; + t[1].verticies = new Vertex[3]; + + Vertex v0 = f.verticies[0]; + Vertex v1 = f.verticies[1]; + Vertex v2 = f.verticies[2]; + Vertex v3 = f.verticies[3]; + + // find the pair of verticies that is closest to each over + // v0 and v2 + // OR + // v1 and v3 + float d1 = v0.v.distanceSquared(v2.v); + float d2 = v1.v.distanceSquared(v3.v); + if (d1 < d2){ + // put an edge in v0, v2 + t[0].verticies[0] = v0; + t[0].verticies[1] = v1; + t[0].verticies[2] = v3; + + t[1].verticies[0] = v1; + t[1].verticies[1] = v2; + t[1].verticies[2] = v3; + }else{ + // put an edge in v1, v3 + t[0].verticies[0] = v0; + t[0].verticies[1] = v1; + t[0].verticies[2] = v2; + + t[1].verticies[0] = v0; + t[1].verticies[1] = v2; + t[1].verticies[2] = v3; + } + + return t; + } + + private ArrayList<Vertex> vertList = new ArrayList<Vertex>(); + + protected void readFace(){ + Face f = new Face(); + vertList.clear(); + + String line = scan.nextLine().trim(); + String[] verticies = line.split("\\s"); + for (String vertex : verticies){ + int v = 0; + int vt = 0; + int vn = 0; + + String[] split = vertex.split("/"); + if (split.length == 1){ + v = Integer.parseInt(split[0].trim()); + }else if (split.length == 2){ + v = Integer.parseInt(split[0].trim()); + vt = Integer.parseInt(split[1].trim()); + }else if (split.length == 3 && !split[1].equals("")){ + v = Integer.parseInt(split[0].trim()); + vt = Integer.parseInt(split[1].trim()); + vn = Integer.parseInt(split[2].trim()); + }else if (split.length == 3){ + v = Integer.parseInt(split[0].trim()); + vn = Integer.parseInt(split[2].trim()); + } + + Vertex vx = new Vertex(); + vx.v = verts.get(v - 1); + + if (vt > 0) + vx.vt = texCoords.get(vt - 1); + + if (vn > 0) + vx.vn = norms.get(vn - 1); + + vertList.add(vx); + } + + if (vertList.size() > 4 || vertList.size() <= 2) + logger.warning("Edge or polygon detected in OBJ. Ignored."); + + f.verticies = new Vertex[vertList.size()]; + for (int i = 0; i < vertList.size(); i++){ + f.verticies[i] = vertList.get(i); + } + + if (matList != null && matFaces.containsKey(currentMatName)){ + matFaces.get(currentMatName).add(f); + }else{ + faces.add(f); // faces that belong to the default material + } + } + + protected Vector3f readVector3(){ + Vector3f v = new Vector3f(); + + v.set(Float.parseFloat(scan.next()), + Float.parseFloat(scan.next()), + Float.parseFloat(scan.next())); + + return v; + } + + protected Vector2f readVector2(){ + Vector2f v = new Vector2f(); + + String line = scan.nextLine().trim(); + String[] split = line.split("\\s"); + v.setX( Float.parseFloat(split[0].trim()) ); + v.setY( Float.parseFloat(split[1].trim()) ); + +// v.setX(scan.nextFloat()); +// if (scan.hasNextFloat()){ +// v.setY(scan.nextFloat()); +// if (scan.hasNextFloat()){ +// scan.nextFloat(); // ignore +// } +// } + + return v; + } + + protected void loadMtlLib(String name) throws IOException{ + if (!name.toLowerCase().endsWith(".mtl")) + throw new IOException("Expected .mtl file! Got: " + name); + + // NOTE: Cut off any relative/absolute paths + name = new File(name).getName(); + AssetKey mtlKey = new AssetKey(key.getFolder() + name); + try { + matList = (MaterialList) assetManager.loadAsset(mtlKey); + } catch (AssetNotFoundException ex){ + logger.log(Level.WARNING, "Cannot locate {0} for model {1}", new Object[]{name, key}); + } + + if (matList != null){ + // create face lists for every material + for (String matName : matList.keySet()){ + matFaces.put(matName, new ArrayList<Face>()); + } + } + } + + protected boolean nextStatement(){ + try { + scan.skip(".*\r{0,1}\n"); + return true; + } catch (NoSuchElementException ex){ + // EOF + return false; + } + } + + protected boolean readLine() throws IOException{ + if (!scan.hasNext()){ + return false; + } + + String cmd = scan.next(); + if (cmd.startsWith("#")){ + // skip entire comment until next line + return nextStatement(); + }else if (cmd.equals("v")){ + // vertex position + verts.add(readVector3()); + }else if (cmd.equals("vn")){ + // vertex normal + norms.add(readVector3()); + }else if (cmd.equals("vt")){ + // texture coordinate + texCoords.add(readVector2()); + }else if (cmd.equals("f")){ + // face, can be triangle, quad, or polygon (unsupported) + readFace(); + }else if (cmd.equals("usemtl")){ + // use material from MTL lib for the following faces + currentMatName = scan.next(); +// if (!matList.containsKey(currentMatName)) +// throw new IOException("Cannot locate material " + currentMatName + " in MTL file!"); + + }else if (cmd.equals("mtllib")){ + // specify MTL lib to use for this OBJ file + String mtllib = scan.nextLine().trim(); + loadMtlLib(mtllib); + }else if (cmd.equals("s") || cmd.equals("g")){ + return nextStatement(); + }else{ + // skip entire command until next line + logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd); + return nextStatement(); + } + + return true; + } + + protected Geometry createGeometry(ArrayList<Face> faceList, String matName) throws IOException{ + if (faceList.isEmpty()) + throw new IOException("No geometry data to generate mesh"); + + // Create mesh from the faces + Mesh mesh = constructMesh(faceList); + + Geometry geom = new Geometry(objName + "-geom-" + (geomIndex++), mesh); + + Material material = null; + if (matName != null && matList != null){ + // Get material from material list + material = matList.get(matName); + } + if (material == null){ + // create default material + material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + material.setFloat("Shininess", 64); + } + geom.setMaterial(material); + if (material.isTransparent()) + geom.setQueueBucket(Bucket.Transparent); + else + geom.setQueueBucket(Bucket.Opaque); + + if (material.getMaterialDef().getName().contains("Lighting") + && mesh.getFloatBuffer(Type.Normal) == null){ + logger.log(Level.WARNING, "OBJ mesh {0} doesn't contain normals! " + + "It might not display correctly", geom.getName()); + } + + return geom; + } + + protected Mesh constructMesh(ArrayList<Face> faceList){ + Mesh m = new Mesh(); + m.setMode(Mode.Triangles); + + boolean hasTexCoord = false; + boolean hasNormals = false; + + ArrayList<Face> newFaces = new ArrayList<Face>(faceList.size()); + for (int i = 0; i < faceList.size(); i++){ + Face f = faceList.get(i); + + for (Vertex v : f.verticies){ + findVertexIndex(v); + + if (!hasTexCoord && v.vt != null) + hasTexCoord = true; + if (!hasNormals && v.vn != null) + hasNormals = true; + } + + if (f.verticies.length == 4){ + Face[] t = quadToTriangle(f); + newFaces.add(t[0]); + newFaces.add(t[1]); + }else{ + newFaces.add(f); + } + } + + FloatBuffer posBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3); + FloatBuffer normBuf = null; + FloatBuffer tcBuf = null; + + if (hasNormals){ + normBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3); + } + if (hasTexCoord){ + tcBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 2); + } + + IndexBuffer indexBuf = null; + if (vertIndexMap.size() >= 65536){ + // too many verticies: use intbuffer instead of shortbuffer + IntBuffer ib = BufferUtils.createIntBuffer(newFaces.size() * 3); + m.setBuffer(VertexBuffer.Type.Index, 3, ib); + indexBuf = new IndexIntBuffer(ib); + }else{ + ShortBuffer sb = BufferUtils.createShortBuffer(newFaces.size() * 3); + m.setBuffer(VertexBuffer.Type.Index, 3, sb); + indexBuf = new IndexShortBuffer(sb); + } + + int numFaces = newFaces.size(); + for (int i = 0; i < numFaces; i++){ + Face f = newFaces.get(i); + if (f.verticies.length != 3) + continue; + + Vertex v0 = f.verticies[0]; + Vertex v1 = f.verticies[1]; + Vertex v2 = f.verticies[2]; + + posBuf.position(v0.index * 3); + posBuf.put(v0.v.x).put(v0.v.y).put(v0.v.z); + posBuf.position(v1.index * 3); + posBuf.put(v1.v.x).put(v1.v.y).put(v1.v.z); + posBuf.position(v2.index * 3); + posBuf.put(v2.v.x).put(v2.v.y).put(v2.v.z); + + if (normBuf != null){ + if (v0.vn != null){ + normBuf.position(v0.index * 3); + normBuf.put(v0.vn.x).put(v0.vn.y).put(v0.vn.z); + normBuf.position(v1.index * 3); + normBuf.put(v1.vn.x).put(v1.vn.y).put(v1.vn.z); + normBuf.position(v2.index * 3); + normBuf.put(v2.vn.x).put(v2.vn.y).put(v2.vn.z); + } + } + + if (tcBuf != null){ + if (v0.vt != null){ + tcBuf.position(v0.index * 2); + tcBuf.put(v0.vt.x).put(v0.vt.y); + tcBuf.position(v1.index * 2); + tcBuf.put(v1.vt.x).put(v1.vt.y); + tcBuf.position(v2.index * 2); + tcBuf.put(v2.vt.x).put(v2.vt.y); + } + } + + int index = i * 3; // current face * 3 = current index + indexBuf.put(index, v0.index); + indexBuf.put(index+1, v1.index); + indexBuf.put(index+2, v2.index); + } + + m.setBuffer(VertexBuffer.Type.Position, 3, posBuf); + m.setBuffer(VertexBuffer.Type.Normal, 3, normBuf); + m.setBuffer(VertexBuffer.Type.TexCoord, 2, tcBuf); + // index buffer was set on creation + + m.setStatic(); + m.updateBound(); + m.updateCounts(); + //m.setInterleaved(); + + // clear data generated face statements + // to prepare for next mesh + vertIndexMap.clear(); + indexVertMap.clear(); + curIndex = 0; + + return m; + } + + @SuppressWarnings("empty-statement") + public Object load(AssetInfo info) throws IOException{ + reset(); + + key = (ModelKey) info.getKey(); + assetManager = info.getManager(); + objName = key.getName(); + + String folderName = key.getFolder(); + String ext = key.getExtension(); + objName = objName.substring(0, objName.length() - ext.length() - 1); + if (folderName != null && folderName.length() > 0){ + objName = objName.substring(folderName.length()); + } + + objNode = new Node(objName + "-objnode"); + + if (!(info.getKey() instanceof ModelKey)) + throw new IllegalArgumentException("Model assets must be loaded using a ModelKey"); + + InputStream in = null; + try { + in = info.openStream(); + + scan = new Scanner(in); + scan.useLocale(Locale.US); + + while (readLine()); + } finally { + if (in != null){ + in.close(); + } + } + + if (matFaces.size() > 0){ + for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){ + ArrayList<Face> materialFaces = entry.getValue(); + if (materialFaces.size() > 0){ + Geometry geom = createGeometry(materialFaces, entry.getKey()); + objNode.attachChild(geom); + } + } + }else if (faces.size() > 0){ + // generate final geometry + Geometry geom = createGeometry(faces, null); + objNode.attachChild(geom); + } + + if (objNode.getQuantity() == 1) + // only 1 geometry, so no need to send node + return objNode.getChild(0); + else + return objNode; + } + +} diff --git a/engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java b/engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java new file mode 100644 index 0000000..f9073b7 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2009-2010 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.shader.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLoader; +import com.jme3.asset.AssetManager; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; + +/** + * GLSL File parser that supports #import pre-processor statement + */ +public class GLSLLoader implements AssetLoader { + + private AssetManager owner; + private Map<String, DependencyNode> dependCache = new HashMap<String, DependencyNode>(); + + private class DependencyNode { + + private String shaderSource; + private String shaderName; + + private final Set<DependencyNode> dependsOn = new HashSet<DependencyNode>(); + private final Set<DependencyNode> dependOnMe = new HashSet<DependencyNode>(); + + public DependencyNode(String shaderName){ + this.shaderName = shaderName; + } + + public void setSource(String source){ + this.shaderSource = source; + } + + public void addDependency(DependencyNode node){ + if (this.dependsOn.contains(node)) + return; // already contains dependency + +// System.out.println(shaderName + " depend on "+node.shaderName); + this.dependsOn.add(node); + node.dependOnMe.add(this); + } + + } + + private class GlslDependKey extends AssetKey<InputStream> { + public GlslDependKey(String name){ + super(name); + } + @Override + public boolean shouldCache(){ + return false; + } + } + + private DependencyNode loadNode(InputStream in, String nodeName) throws IOException{ + DependencyNode node = new DependencyNode(nodeName); + if (in == null) + throw new IOException("Dependency "+nodeName+" cannot be found."); + + StringBuilder sb = new StringBuilder(); + BufferedReader r = new BufferedReader(new InputStreamReader(in)); + while (r.ready()){ + String ln = r.readLine(); + if (ln.startsWith("#import ")){ + ln = ln.substring(8).trim(); + if (ln.startsWith("\"") && ln.endsWith("\"") && ln.length() > 3){ + // import user code + // remove quotes to get filename + ln = ln.substring(1, ln.length()-1); + if (ln.equals(nodeName)) + throw new IOException("Node depends on itself."); + + // check cache first + DependencyNode dependNode = dependCache.get(ln); + if (dependNode == null){ + GlslDependKey key = new GlslDependKey(ln); + // make sure not to register an input stream with + // the cache.. + InputStream stream = (InputStream) owner.loadAsset(key); + dependNode = loadNode(stream, ln); + } + node.addDependency(dependNode); + } +// }else if (ln.startsWith("uniform") || ln.startsWith("varying") || ln.startsWith("attribute")){ +// // these variables are included as dependencies as well +// DependencyNode dependNode = dependCache.get(ln); +// if (dependNode == null){ +// // the source and name are the same for variable dependencies +// dependNode = new DependencyNode(ln); +// dependNode.setSource(ln); +// dependCache.put(ln, dependNode); +// } +// node.addDependency(dependNode); + }else{ + sb.append(ln).append('\n'); + } + } + r.close(); + + node.setSource(sb.toString()); + dependCache.put(nodeName, node); + return node; + } + + private DependencyNode nextIndependentNode(List<DependencyNode> checkedNodes){ + Collection<DependencyNode> allNodes = dependCache.values(); + if (allNodes == null || allNodes.isEmpty()) + return null; + + for (DependencyNode node : allNodes){ + if (node.dependsOn.isEmpty()){ + return node; + } + } + + // circular dependency found.. + for (DependencyNode node : allNodes){ + System.out.println(node.shaderName); + } + throw new RuntimeException("Circular dependency."); + } + + private String resolveDependencies(DependencyNode root){ + StringBuilder sb = new StringBuilder(); + + List<DependencyNode> checkedNodes = new ArrayList<DependencyNode>(); + checkedNodes.add(root); + while (true){ + DependencyNode indepnNode = nextIndependentNode(checkedNodes); + if (indepnNode == null) + break; + + sb.append(indepnNode.shaderSource).append('\n'); + dependCache.remove(indepnNode.shaderName); + + // take out this dependency + for (Iterator<DependencyNode> iter = indepnNode.dependOnMe.iterator(); + iter.hasNext();){ + DependencyNode dependNode = iter.next(); + iter.remove(); + dependNode.dependsOn.remove(indepnNode); + } + } + +// System.out.println(sb.toString()); +// System.out.println("--------------------------------------------------"); + + return sb.toString(); + } + + /** + * + * @param owner + * @param in + * @param extension + * @param key + * @return + * @throws java.io.IOException + */ + public Object load(AssetInfo info) throws IOException { + // The input stream provided is for the vertex shader, + // to retrieve the fragment shader, use the content manager + this.owner = info.getManager(); + if (info.getKey().getExtension().equals("glsllib")){ + // NOTE: Loopback, GLSLLIB is loaded by this loader + // and needs data as InputStream + return info.openStream(); + }else{ + // GLSLLoader wants result as String for + // fragment shader + DependencyNode rootNode = loadNode(info.openStream(), "[main]"); + String code = resolveDependencies(rootNode); + dependCache.clear(); + return code; + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java new file mode 100644 index 0000000..897c9eb --- /dev/null +++ b/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java @@ -0,0 +1,827 @@ +/* + * Copyright (c) 2009-2010 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; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetLoader; +import com.jme3.asset.TextureKey; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture.Type; +import com.jme3.util.BufferUtils; +import com.jme3.util.LittleEndien; +import java.io.DataInput; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * <code>DDSLoader</code> is an image loader that reads in a DirectX DDS file. + * Supports DXT1, DXT3, DXT5, RGB, RGBA, Grayscale, Alpha pixel formats. + * 2D images, mipmapped 2D images, and cubemaps. + * + * @author Gareth Jenkins-Jones + * @author Kirill Vainer + * @version $Id: DDSLoader.java,v 2.0 2008/8/15 + */ +public class DDSLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(DDSLoader.class.getName()); + private static final boolean forceRGBA = false; + private static final int DDSD_MANDATORY = 0x1007; + private static final int DDSD_MANDATORY_DX10 = 0x6; + private static final int DDSD_MIPMAPCOUNT = 0x20000; + private static final int DDSD_LINEARSIZE = 0x80000; + private static final int DDSD_DEPTH = 0x800000; + private static final int DDPF_ALPHAPIXELS = 0x1; + private static final int DDPF_FOURCC = 0x4; + private static final int DDPF_RGB = 0x40; + // used by compressonator to mark grayscale images, red channel mask is used for data and bitcount is 8 + private static final int DDPF_GRAYSCALE = 0x20000; + // used by compressonator to mark alpha images, alpha channel mask is used for data and bitcount is 8 + private static final int DDPF_ALPHA = 0x2; + // used by NVTextureTools to mark normal images. + private static final int DDPF_NORMAL = 0x80000000; + private static final int SWIZZLE_xGxR = 0x78477852; + private static final int DDSCAPS_COMPLEX = 0x8; + private static final int DDSCAPS_TEXTURE = 0x1000; + private static final int DDSCAPS_MIPMAP = 0x400000; + private static final int DDSCAPS2_CUBEMAP = 0x200; + private static final int DDSCAPS2_VOLUME = 0x200000; + private static final int PF_DXT1 = 0x31545844; + private static final int PF_DXT3 = 0x33545844; + private static final int PF_DXT5 = 0x35545844; + private static final int PF_ATI1 = 0x31495441; + private static final int PF_ATI2 = 0x32495441; // 0x41544932; + private static final int PF_DX10 = 0x30315844; // a DX10 format + private static final int DX10DIM_BUFFER = 0x1, + DX10DIM_TEXTURE1D = 0x2, + DX10DIM_TEXTURE2D = 0x3, + DX10DIM_TEXTURE3D = 0x4; + private static final int DX10MISC_GENERATE_MIPS = 0x1, + DX10MISC_TEXTURECUBE = 0x4; + private static final double LOG2 = Math.log(2); + private int width; + private int height; + private int depth; + private int flags; + private int pitchOrSize; + private int mipMapCount; + private int caps1; + private int caps2; + private boolean directx10; + private boolean compressed; + private boolean texture3D; + private boolean grayscaleOrAlpha; + private boolean normal; + private Format pixelFormat; + private int bpp; + private int[] sizes; + private int redMask, greenMask, blueMask, alphaMask; + private DataInput in; + + public DDSLoader() { + } + + public Object load(AssetInfo info) throws IOException { + if (!(info.getKey() instanceof TextureKey)) { + throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey"); + } + + InputStream stream = null; + try { + stream = info.openStream(); + in = new LittleEndien(stream); + loadHeader(); + if (texture3D) { + ((TextureKey) info.getKey()).setTextureTypeHint(Type.ThreeDimensional); + } else if (depth > 1) { + ((TextureKey) info.getKey()).setTextureTypeHint(Type.CubeMap); + } + ArrayList<ByteBuffer> data = readData(((TextureKey) info.getKey()).isFlipY()); + return new Image(pixelFormat, width, height, depth, data, sizes); + } finally { + if (stream != null){ + stream.close(); + } + } + } + + public Image load(InputStream stream) throws IOException { + in = new LittleEndien(stream); + loadHeader(); + ArrayList<ByteBuffer> data = readData(false); + return new Image(pixelFormat, width, height, depth, data, sizes); + } + + private void loadDX10Header() throws IOException { + int dxgiFormat = in.readInt(); + if (dxgiFormat != 83) { + throw new IOException("Only DXGI_FORMAT_BC5_UNORM " + + "is supported for DirectX10 DDS! Got: " + dxgiFormat); + } + pixelFormat = Format.LATC; + bpp = 8; + compressed = true; + + int resDim = in.readInt(); + if (resDim == DX10DIM_TEXTURE3D) { + texture3D = true; + } + int miscFlag = in.readInt(); + int arraySize = in.readInt(); + if (is(miscFlag, DX10MISC_TEXTURECUBE)) { + // mark texture as cube + if (arraySize != 6) { + throw new IOException("Cubemaps should consist of 6 images!"); + } + } + + in.skipBytes(4); // skip reserved value + } + + /** + * Reads the header (first 128 bytes) of a DDS File + */ + private void loadHeader() throws IOException { + if (in.readInt() != 0x20534444 || in.readInt() != 124) { + throw new IOException("Not a DDS file"); + } + + flags = in.readInt(); + + if (!is(flags, DDSD_MANDATORY) && !is(flags, DDSD_MANDATORY_DX10)) { + throw new IOException("Mandatory flags missing"); + } + + height = in.readInt(); + width = in.readInt(); + pitchOrSize = in.readInt(); + depth = in.readInt(); + mipMapCount = in.readInt(); + in.skipBytes(44); + pixelFormat = null; + directx10 = false; + readPixelFormat(); + caps1 = in.readInt(); + caps2 = in.readInt(); + in.skipBytes(12); + texture3D = false; + + if (!directx10) { + if (!is(caps1, DDSCAPS_TEXTURE)) { + throw new IOException("File is not a texture"); + } + + if (depth <= 0) { + depth = 1; + } + + if (is(caps2, DDSCAPS2_CUBEMAP)) { + depth = 6; // somewhat of a hack, force loading 6 textures if a cubemap + } + + if (is(caps2, DDSCAPS2_VOLUME)) { + texture3D = true; + } + } + + int expectedMipmaps = 1 + (int) Math.ceil(Math.log(Math.max(height, width)) / LOG2); + + if (is(caps1, DDSCAPS_MIPMAP)) { + if (!is(flags, DDSD_MIPMAPCOUNT)) { + mipMapCount = expectedMipmaps; + } else if (mipMapCount != expectedMipmaps) { + // changed to warning- images often do not have the required amount, + // or specify that they have mipmaps but include only the top level.. + logger.log(Level.WARNING, "Got {0} mipmaps, expected {1}", + new Object[]{mipMapCount, expectedMipmaps}); + } + } else { + mipMapCount = 1; + } + + if (directx10) { + loadDX10Header(); + } + + loadSizes(); + } + + /** + * Reads the PixelFormat structure in a DDS file + */ + private void readPixelFormat() throws IOException { + int pfSize = in.readInt(); + if (pfSize != 32) { + throw new IOException("Pixel format size is " + pfSize + ", not 32"); + } + + int pfFlags = in.readInt(); + normal = is(pfFlags, DDPF_NORMAL); + + if (is(pfFlags, DDPF_FOURCC)) { + compressed = true; + int fourcc = in.readInt(); + int swizzle = in.readInt(); + in.skipBytes(16); + + switch (fourcc) { + case PF_DXT1: + bpp = 4; + if (is(pfFlags, DDPF_ALPHAPIXELS)) { + pixelFormat = Image.Format.DXT1A; + } else { + pixelFormat = Image.Format.DXT1; + } + break; + case PF_DXT3: + bpp = 8; + pixelFormat = Image.Format.DXT3; + break; + case PF_DXT5: + bpp = 8; + pixelFormat = Image.Format.DXT5; + if (swizzle == SWIZZLE_xGxR) { + normal = true; + } + break; + case PF_ATI1: + bpp = 4; + pixelFormat = Image.Format.LTC; + break; + case PF_ATI2: + bpp = 8; + pixelFormat = Image.Format.LATC; + break; + case PF_DX10: + compressed = false; + directx10 = true; + // exit here, the rest of the structure is not valid + // the real format will be available in the DX10 header + return; + default: + throw new IOException("Unknown fourcc: " + string(fourcc) + ", " + Integer.toHexString(fourcc)); + } + + int size = ((width + 3) / 4) * ((height + 3) / 4) * bpp * 2; + + if (is(flags, DDSD_LINEARSIZE)) { + if (pitchOrSize == 0) { + logger.warning("Must use linear size with fourcc"); + pitchOrSize = size; + } else if (pitchOrSize != size) { + logger.log(Level.WARNING, "Expected size = {0}, real = {1}", + new Object[]{size, pitchOrSize}); + } + } else { + pitchOrSize = size; + } + } else { + compressed = false; + + // skip fourCC + in.readInt(); + + bpp = in.readInt(); + redMask = in.readInt(); + greenMask = in.readInt(); + blueMask = in.readInt(); + alphaMask = in.readInt(); + + if (is(pfFlags, DDPF_RGB)) { + if (is(pfFlags, DDPF_ALPHAPIXELS)) { + pixelFormat = Format.RGBA8; + } else { + pixelFormat = Format.RGB8; + } + } else if (is(pfFlags, DDPF_GRAYSCALE) && is(pfFlags, DDPF_ALPHAPIXELS)) { + switch (bpp) { + case 16: + pixelFormat = Format.Luminance8Alpha8; + break; + case 32: + pixelFormat = Format.Luminance16Alpha16; + break; + default: + throw new IOException("Unsupported GrayscaleAlpha BPP: " + bpp); + } + grayscaleOrAlpha = true; + } else if (is(pfFlags, DDPF_GRAYSCALE)) { + switch (bpp) { + case 8: + pixelFormat = Format.Luminance8; + break; + case 16: + pixelFormat = Format.Luminance16; + break; + default: + throw new IOException("Unsupported Grayscale BPP: " + bpp); + } + grayscaleOrAlpha = true; + } else if (is(pfFlags, DDPF_ALPHA)) { + switch (bpp) { + case 8: + pixelFormat = Format.Alpha8; + break; + case 16: + pixelFormat = Format.Alpha16; + break; + default: + throw new IOException("Unsupported Alpha BPP: " + bpp); + } + grayscaleOrAlpha = true; + } else { + throw new IOException("Unknown PixelFormat in DDS file"); + } + + int size = (bpp / 8 * width); + + if (is(flags, DDSD_LINEARSIZE)) { + if (pitchOrSize == 0) { + logger.warning("Linear size said to contain valid value but does not"); + pitchOrSize = size; + } else if (pitchOrSize != size) { + logger.log(Level.WARNING, "Expected size = {0}, real = {1}", + new Object[]{size, pitchOrSize}); + } + } else { + pitchOrSize = size; + } + } + } + + /** + * Computes the sizes of each mipmap level in bytes, and stores it in sizes_[]. + */ + private void loadSizes() { + int mipWidth = width; + int mipHeight = height; + + sizes = new int[mipMapCount]; + int outBpp = pixelFormat.getBitsPerPixel(); + for (int i = 0; i < mipMapCount; i++) { + int size; + if (compressed) { + size = ((mipWidth + 3) / 4) * ((mipHeight + 3) / 4) * outBpp * 2; + } else { + size = mipWidth * mipHeight * outBpp / 8; + } + + sizes[i] = ((size + 3) / 4) * 4; + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + } + + /** + * Flips the given image data on the Y axis. + * @param data Data array containing image data (without mipmaps) + * @param scanlineSize Size of a single scanline = width * bytesPerPixel + * @param height Height of the image in pixels + * @return The new data flipped by the Y axis + */ + public byte[] flipData(byte[] data, int scanlineSize, int height) { + byte[] newData = new byte[data.length]; + + for (int y = 0; y < height; y++) { + System.arraycopy(data, y * scanlineSize, + newData, (height - y - 1) * scanlineSize, + scanlineSize); + } + + return newData; + } + + /** + * Reads a grayscale image with mipmaps from the InputStream + * @param flip Flip the loaded image by Y axis + * @param totalSize Total size of the image in bytes including the mipmaps + * @return A ByteBuffer containing the grayscale image data with mips. + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readGrayscale2D(boolean flip, int totalSize) throws IOException { + ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); + + if (bpp == 8) { + logger.finest("Source image format: R8"); + } + + assert bpp == pixelFormat.getBitsPerPixel(); + + int mipWidth = width; + int mipHeight = height; + + for (int mip = 0; mip < mipMapCount; mip++) { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + if (flip) { + data = flipData(data, mipWidth * bpp / 8, mipHeight); + } + buffer.put(data); + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + + return buffer; + } + + /** + * Reads an uncompressed RGB or RGBA image. + * + * @param flip Flip the image on the Y axis + * @param totalSize Size of the image in bytes including mipmaps + * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_ + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readRGB2D(boolean flip, int totalSize) throws IOException { + int redCount = count(redMask), + blueCount = count(blueMask), + greenCount = count(greenMask), + alphaCount = count(alphaMask); + + if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) { + if (alphaMask == 0xFF000000 && bpp == 32) { + logger.finest("Data source format: BGRA8"); + } else if (bpp == 24) { + logger.finest("Data source format: BGR8"); + } + } + + int sourcebytesPP = bpp / 8; + int targetBytesPP = pixelFormat.getBitsPerPixel() / 8; + + ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize); + + int mipWidth = width; + int mipHeight = height; + + int offset = 0; + byte[] b = new byte[sourcebytesPP]; + for (int mip = 0; mip < mipMapCount; mip++) { + for (int y = 0; y < mipHeight; y++) { + for (int x = 0; x < mipWidth; x++) { + in.readFully(b); + + int i = byte2int(b); + + byte red = (byte) (((i & redMask) >> redCount)); + byte green = (byte) (((i & greenMask) >> greenCount)); + byte blue = (byte) (((i & blueMask) >> blueCount)); + byte alpha = (byte) (((i & alphaMask) >> alphaCount)); + + if (flip) { + dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP); + } + //else + // dataBuffer.position(offset + (y * width + x) * targetBytesPP); + + if (alphaMask == 0) { + dataBuffer.put(red).put(green).put(blue); + } else { + dataBuffer.put(red).put(green).put(blue).put(alpha); + } + } + } + + offset += mipWidth * mipHeight * targetBytesPP; + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + + return dataBuffer; + } + + /** + * Reads a DXT compressed image from the InputStream + * + * @param totalSize Total size of the image in bytes, including mipmaps + * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_ + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readDXT2D(boolean flip, int totalSize) throws IOException { + logger.finest("Source image format: DXT"); + + ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); + + int mipWidth = width; + int mipHeight = height; + + for (int mip = 0; mip < mipMapCount; mip++) { + if (flip) { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + ByteBuffer wrapped = ByteBuffer.wrap(data); + wrapped.rewind(); + ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat); + buffer.put(flipped); + } else { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + buffer.put(data); + } + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + buffer.rewind(); + + return buffer; + } + + /** + * Reads a grayscale image with mipmaps from the InputStream + * @param flip Flip the loaded image by Y axis + * @param totalSize Total size of the image in bytes including the mipmaps + * @return A ByteBuffer containing the grayscale image data with mips. + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readGrayscale3D(boolean flip, int totalSize) throws IOException { + ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize * depth); + + if (bpp == 8) { + logger.finest("Source image format: R8"); + } + + assert bpp == pixelFormat.getBitsPerPixel(); + + + for (int i = 0; i < depth; i++) { + int mipWidth = width; + int mipHeight = height; + + for (int mip = 0; mip < mipMapCount; mip++) { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + if (flip) { + data = flipData(data, mipWidth * bpp / 8, mipHeight); + } + buffer.put(data); + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + } + buffer.rewind(); + return buffer; + } + + /** + * Reads an uncompressed RGB or RGBA image. + * + * @param flip Flip the image on the Y axis + * @param totalSize Size of the image in bytes including mipmaps + * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_ + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readRGB3D(boolean flip, int totalSize) throws IOException { + int redCount = count(redMask), + blueCount = count(blueMask), + greenCount = count(greenMask), + alphaCount = count(alphaMask); + + if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) { + if (alphaMask == 0xFF000000 && bpp == 32) { + logger.finest("Data source format: BGRA8"); + } else if (bpp == 24) { + logger.finest("Data source format: BGR8"); + } + } + + int sourcebytesPP = bpp / 8; + int targetBytesPP = pixelFormat.getBitsPerPixel() / 8; + + ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize * depth); + + for (int k = 0; k < depth; k++) { + // ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize); + int mipWidth = width; + int mipHeight = height; + int offset = k * totalSize; + byte[] b = new byte[sourcebytesPP]; + for (int mip = 0; mip < mipMapCount; mip++) { + for (int y = 0; y < mipHeight; y++) { + for (int x = 0; x < mipWidth; x++) { + in.readFully(b); + + int i = byte2int(b); + + byte red = (byte) (((i & redMask) >> redCount)); + byte green = (byte) (((i & greenMask) >> greenCount)); + byte blue = (byte) (((i & blueMask) >> blueCount)); + byte alpha = (byte) (((i & alphaMask) >> alphaCount)); + + if (flip) { + dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP); + } + //else + // dataBuffer.position(offset + (y * width + x) * targetBytesPP); + + if (alphaMask == 0) { + dataBuffer.put(red).put(green).put(blue); + } else { + dataBuffer.put(red).put(green).put(blue).put(alpha); + } + } + } + + offset += (mipWidth * mipHeight * targetBytesPP); + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + } + dataBuffer.rewind(); + return dataBuffer; + } + + /** + * Reads a DXT compressed image from the InputStream + * + * @param totalSize Total size of the image in bytes, including mipmaps + * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_ + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readDXT3D(boolean flip, int totalSize) throws IOException { + logger.finest("Source image format: DXT"); + + ByteBuffer bufferAll = BufferUtils.createByteBuffer(totalSize * depth); + + for (int i = 0; i < depth; i++) { + ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); + int mipWidth = width; + int mipHeight = height; + for (int mip = 0; mip < mipMapCount; mip++) { + if (flip) { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + ByteBuffer wrapped = ByteBuffer.wrap(data); + wrapped.rewind(); + ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat); + flipped.rewind(); + buffer.put(flipped); + } else { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + buffer.put(data); + } + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + buffer.rewind(); + bufferAll.put(buffer); + } + + return bufferAll; + } + + /** + * Reads the image data from the InputStream in the required format. + * If the file contains a cubemap image, it is loaded as 6 ByteBuffers + * (potentially containing mipmaps if they were specified), otherwise + * a single ByteBuffer is returned for a 2D image. + * + * @param flip Flip the image data or not. + * For cubemaps, each of the cubemap faces is flipped individually. + * If the image is DXT compressed, no flipping is done. + * @return An ArrayList containing a single ByteBuffer for a 2D image, or 6 ByteBuffers for a cubemap. + * The cubemap ByteBuffer order is PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ. + * + * @throws java.io.IOException If an error occured while reading from the stream. + */ + public ArrayList<ByteBuffer> readData(boolean flip) throws IOException { + int totalSize = 0; + + for (int i = 0; i < sizes.length; i++) { + totalSize += sizes[i]; + } + + ArrayList<ByteBuffer> allMaps = new ArrayList<ByteBuffer>(); + if (depth > 1 && !texture3D) { + for (int i = 0; i < depth; i++) { + if (compressed) { + allMaps.add(readDXT2D(flip, totalSize)); + } else if (grayscaleOrAlpha) { + allMaps.add(readGrayscale2D(flip, totalSize)); + } else { + allMaps.add(readRGB2D(flip, totalSize)); + } + } + } else if (texture3D) { + if (compressed) { + allMaps.add(readDXT3D(flip, totalSize)); + } else if (grayscaleOrAlpha) { + allMaps.add(readGrayscale3D(flip, totalSize)); + } else { + allMaps.add(readRGB3D(flip, totalSize)); + } + + } else { + if (compressed) { + allMaps.add(readDXT2D(flip, totalSize)); + } else if (grayscaleOrAlpha) { + allMaps.add(readGrayscale2D(flip, totalSize)); + } else { + allMaps.add(readRGB2D(flip, totalSize)); + } + } + + return allMaps; + } + + /** + * Checks if flags contains the specified mask + */ + private static boolean is(int flags, int mask) { + return (flags & mask) == mask; + } + + /** + * Counts the amount of bits needed to shift till bitmask n is at zero + * @param n Bitmask to test + */ + private static int count(int n) { + if (n == 0) { + return 0; + } + + int i = 0; + while ((n & 0x1) == 0) { + n = n >> 1; + i++; + if (i > 32) { + throw new RuntimeException(Integer.toHexString(n)); + } + } + + return i; + } + + /** + * Converts a 1 to 4 sized byte array to an integer + */ + private static int byte2int(byte[] b) { + if (b.length == 1) { + return b[0] & 0xFF; + } else if (b.length == 2) { + return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8); + } else if (b.length == 3) { + return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8) | ((b[2] & 0xFF) << 16); + } else if (b.length == 4) { + return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8) | ((b[2] & 0xFF) << 16) | ((b[3] & 0xFF) << 24); + } else { + return 0; + } + } + + /** + * Converts a int representing a FourCC into a String + */ + private static String string(int value) { + StringBuilder buf = new StringBuilder(); + + buf.append((char) (value & 0xFF)); + buf.append((char) ((value & 0xFF00) >> 8)); + buf.append((char) ((value & 0xFF0000) >> 16)); + buf.append((char) ((value & 0xFF00000) >> 24)); + + return buf.toString(); + } +} diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java b/engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java new file mode 100644 index 0000000..ff90f03 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2009-2010 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; + +import com.jme3.math.FastMath; +import com.jme3.texture.Image.Format; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * DXTFlipper is a utility class used to flip along Y axis DXT compressed textures. + * + * @author Kirill Vainer + */ +public class DXTFlipper { + + private static final ByteBuffer bb = ByteBuffer.allocate(8); + + static { + bb.order(ByteOrder.LITTLE_ENDIAN); + } + + private static long readCode5(long data, int x, int y){ + long shift = (4 * y + x) * 3; + long mask = 0x7; + mask <<= shift; + long code = data & mask; + code >>= shift; + return code; + } + + private static long writeCode5(long data, int x, int y, long code){ + long shift = (4 * y + x) * 3; + long mask = 0x7; + code = (code & mask) << shift; + mask <<= shift; + mask = ~mask; + data &= mask; + data |= code; // write new code + return data; + } + + private static void flipDXT5Block(byte[] block, int h){ + if (h == 1) + return; + + byte c0 = block[0]; + byte c1 = block[1]; + + bb.clear(); + bb.put(block, 2, 6).flip(); + bb.clear(); + long l = bb.getLong(); + long n = l; + + if (h == 2){ + n = writeCode5(n, 0, 0, readCode5(l, 0, 1)); + n = writeCode5(n, 1, 0, readCode5(l, 1, 1)); + n = writeCode5(n, 2, 0, readCode5(l, 2, 1)); + n = writeCode5(n, 3, 0, readCode5(l, 3, 1)); + + n = writeCode5(n, 0, 1, readCode5(l, 0, 0)); + n = writeCode5(n, 1, 1, readCode5(l, 1, 0)); + n = writeCode5(n, 2, 1, readCode5(l, 2, 0)); + n = writeCode5(n, 3, 1, readCode5(l, 3, 0)); + }else{ + n = writeCode5(n, 0, 0, readCode5(l, 0, 3)); + n = writeCode5(n, 1, 0, readCode5(l, 1, 3)); + n = writeCode5(n, 2, 0, readCode5(l, 2, 3)); + n = writeCode5(n, 3, 0, readCode5(l, 3, 3)); + + n = writeCode5(n, 0, 1, readCode5(l, 0, 2)); + n = writeCode5(n, 1, 1, readCode5(l, 1, 2)); + n = writeCode5(n, 2, 1, readCode5(l, 2, 2)); + n = writeCode5(n, 3, 1, readCode5(l, 3, 2)); + + n = writeCode5(n, 0, 2, readCode5(l, 0, 1)); + n = writeCode5(n, 1, 2, readCode5(l, 1, 1)); + n = writeCode5(n, 2, 2, readCode5(l, 2, 1)); + n = writeCode5(n, 3, 2, readCode5(l, 3, 1)); + + n = writeCode5(n, 0, 3, readCode5(l, 0, 0)); + n = writeCode5(n, 1, 3, readCode5(l, 1, 0)); + n = writeCode5(n, 2, 3, readCode5(l, 2, 0)); + n = writeCode5(n, 3, 3, readCode5(l, 3, 0)); + } + + bb.clear(); + bb.putLong(n); + bb.clear(); + bb.get(block, 2, 6).flip(); + + assert c0 == block[0] && c1 == block[1]; + } + + private static void flipDXT3Block(byte[] block, int h){ + if (h == 1) + return; + + // first row + byte tmp0 = block[0]; + byte tmp1 = block[1]; + + if (h == 2){ + block[0] = block[2]; + block[1] = block[3]; + + block[2] = tmp0; + block[3] = tmp1; + }else{ + // write last row to first row + block[0] = block[6]; + block[1] = block[7]; + + // write first row to last row + block[6] = tmp0; + block[7] = tmp1; + + // 2nd row + tmp0 = block[2]; + tmp1 = block[3]; + + // write 3rd row to 2nd + block[2] = block[4]; + block[3] = block[5]; + + // write 2nd row to 3rd + block[4] = tmp0; + block[5] = tmp1; + } + } + + /** + * Flips a DXT color block or a DXT3 alpha block + * @param block + * @param h + */ + private static void flipDXT1orDXTA3Block(byte[] block, int h){ + byte tmp; + switch (h){ + case 1: + return; + case 2: + // keep header intact (the two colors) + // header takes 4 bytes + + // flip only two top rows + tmp = block[4+1]; + block[4+1] = block[4+0]; + block[4+0] = tmp; + return; + default: + // keep header intact (the two colors) + // header takes 4 bytes + + // flip first & fourth row + tmp = block[4+3]; + block[4+3] = block[4+0]; + block[4+0] = tmp; + + // flip second and third row + tmp = block[4+2]; + block[4+2] = block[4+1]; + block[4+1] = tmp; + return; + } + } + + public static ByteBuffer flipDXT(ByteBuffer img, int w, int h, Format format){ + int blocksX = (int) FastMath.ceil((float)w / 4f); + int blocksY = (int) FastMath.ceil((float)h / 4f); + + int type; + switch (format){ + case DXT1: + case DXT1A: + type = 1; + break; + case DXT3: + type = 2; + break; + case DXT5: + type = 3; + break; + case LATC: + type = 4; + break; + case LTC: + type = 5; + break; + default: + throw new IllegalArgumentException(); + } + + // DXT1 uses 8 bytes per block, + // DXT3, DXT5, LATC use 16 bytes per block + int bpb = type == 1 || type == 5 ? 8 : 16; + + ByteBuffer retImg = BufferUtils.createByteBuffer(blocksX * blocksY * bpb); + + if (h == 1){ + retImg.put(img); + retImg.rewind(); + return retImg; + }else if (h == 2){ + byte[] colorBlock = new byte[8]; + byte[] alphaBlock = type != 1 && type != 5 ? new byte[8] : null; + for (int x = 0; x < blocksX; x++){ + // prepeare for block reading + int blockByteOffset = x * bpb; + img.position(blockByteOffset); + img.limit(blockByteOffset + bpb); + + img.get(colorBlock); + if (type == 4 || type == 5) + flipDXT5Block(colorBlock, h); + else + flipDXT1orDXTA3Block(colorBlock, h); + + // write block (no need to flip block indexes, only pixels + // inside block + retImg.put(colorBlock); + + if (alphaBlock != null){ + img.get(alphaBlock); + switch (type){ + case 2: + flipDXT3Block(alphaBlock, h); break; + case 3: + case 4: + flipDXT5Block(alphaBlock, h); + break; + } + retImg.put(alphaBlock); + } + } + retImg.rewind(); + return retImg; + }else if (h >= 4){ + byte[] colorBlock = new byte[8]; + byte[] alphaBlock = type != 1 && type != 5 ? new byte[8] : null; + for (int y = 0; y < blocksY; y++){ + for (int x = 0; x < blocksX; x++){ + // prepeare for block reading + int blockIdx = y * blocksX + x; + int blockByteOffset = blockIdx * bpb; + + img.position(blockByteOffset); + img.limit(blockByteOffset + bpb); + + blockIdx = (blocksY - y - 1) * blocksX + x; + blockByteOffset = blockIdx * bpb; + + retImg.position(blockByteOffset); + retImg.limit(blockByteOffset + bpb); + + if (alphaBlock != null){ + img.get(alphaBlock); + switch (type){ + case 2: + flipDXT3Block(alphaBlock, h); + break; + case 3: + case 4: + flipDXT5Block(alphaBlock, h); + break; + } + retImg.put(alphaBlock); + } + + img.get(colorBlock); + if (type == 4 || type == 5) + flipDXT5Block(colorBlock, h); + else + flipDXT1orDXTA3Block(colorBlock, h); + + retImg.put(colorBlock); + } + } + retImg.limit(retImg.capacity()); + retImg.position(0); + return retImg; + }else{ + return null; + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java new file mode 100644 index 0000000..4758ecf --- /dev/null +++ b/engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2009-2010 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; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetLoader; +import com.jme3.asset.TextureKey; +import com.jme3.math.FastMath; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.util.BufferUtils; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HDRLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(HDRLoader.class.getName()); + + private boolean writeRGBE = false; + private ByteBuffer rleTempBuffer; + private ByteBuffer dataStore; + private final float[] tempF = new float[3]; + + public HDRLoader(boolean writeRGBE){ + this.writeRGBE = writeRGBE; + } + + public HDRLoader(){ + } + + public static void convertFloatToRGBE(byte[] rgbe, float red, float green, float blue){ + double max = red; + if (green > max) max = green; + if (blue > max) max = blue; + if (max < 1.0e-32){ + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + }else{ + double exp = Math.ceil( Math.log10(max) / Math.log10(2) ); + double divider = Math.pow(2.0, exp); + rgbe[0] = (byte) ((red / divider) * 255.0); + rgbe[1] = (byte) ((green / divider) * 255.0); + rgbe[2] = (byte) ((blue / divider) * 255.0); + rgbe[3] = (byte) (exp + 128.0); + } + } + + public static void convertRGBEtoFloat(byte[] rgbe, float[] rgbf){ + int R = rgbe[0] & 0xFF, + G = rgbe[1] & 0xFF, + B = rgbe[2] & 0xFF, + E = rgbe[3] & 0xFF; + + float e = (float) Math.pow(2f, E - (128 + 8) ); + rgbf[0] = R * e; + rgbf[1] = G * e; + rgbf[2] = B * e; + } + + public static void convertRGBEtoFloat2(byte[] rgbe, float[] rgbf){ + int R = rgbe[0] & 0xFF, + G = rgbe[1] & 0xFF, + B = rgbe[2] & 0xFF, + E = rgbe[3] & 0xFF; + + float e = (float) Math.pow(2f, E - 128); + rgbf[0] = (R / 256.0f) * e; + rgbf[1] = (G / 256.0f) * e; + rgbf[2] = (B / 256.0f) * e; + } + + public static void convertRGBEtoFloat3(byte[] rgbe, float[] rgbf){ + int R = rgbe[0] & 0xFF, + G = rgbe[1] & 0xFF, + B = rgbe[2] & 0xFF, + E = rgbe[3] & 0xFF; + + float e = (float) Math.pow(2f, E - (128 + 8) ); + rgbf[0] = R * e; + rgbf[1] = G * e; + rgbf[2] = B * e; + } + + private short flip(int in){ + return (short) ((in << 8 & 0xFF00) | (in >> 8)); + } + + private void writeRGBE(byte[] rgbe){ + if (writeRGBE){ + dataStore.put(rgbe); + }else{ + convertRGBEtoFloat(rgbe, tempF); + dataStore.putShort(FastMath.convertFloatToHalf(tempF[0])) + .putShort(FastMath.convertFloatToHalf(tempF[1])). + putShort(FastMath.convertFloatToHalf(tempF[2])); + } + } + + private String readString(InputStream is) throws IOException{ + StringBuilder sb = new StringBuilder(); + while (true){ + int i = is.read(); + if (i == 0x0a || i == -1) // new line or EOF + return sb.toString(); + + sb.append((char)i); + } + } + + private boolean decodeScanlineRLE(InputStream in, int width) throws IOException{ + // must deocde RLE data into temp buffer before converting to float + if (rleTempBuffer == null){ + rleTempBuffer = BufferUtils.createByteBuffer(width * 4); + }else{ + rleTempBuffer.clear(); + if (rleTempBuffer.remaining() < width * 4) + rleTempBuffer = BufferUtils.createByteBuffer(width * 4); + } + + // read each component seperately + for (int i = 0; i < 4; i++) { + // read WIDTH bytes for the channel + for (int j = 0; j < width;) { + int code = in.read(); + if (code > 128) { // run + code -= 128; + int val = in.read(); + while ((code--) != 0) { + rleTempBuffer.put( (j++) * 4 + i , (byte)val); + //scanline[j++][i] = val; + } + } else { // non-run + while ((code--) != 0) { + int val = in.read(); + rleTempBuffer.put( (j++) * 4 + i, (byte)val); + //scanline[j++][i] = in.read(); + } + } + } + } + + rleTempBuffer.rewind(); + byte[] rgbe = new byte[4]; +// float[] temp = new float[3]; + + // decode temp buffer into float data + for (int i = 0; i < width; i++){ + rleTempBuffer.get(rgbe); + writeRGBE(rgbe); + } + + return true; + } + + private boolean decodeScanlineUncompressed(InputStream in, int width) throws IOException{ + byte[] rgbe = new byte[4]; + + for (int i = 0; i < width; i+=3){ + if (in.read(rgbe) < 1) + return false; + + writeRGBE(rgbe); + } + return true; + } + + private void decodeScanline(InputStream in, int width) throws IOException{ + if (width < 8 || width > 0x7fff){ + // too short/long for RLE compression + decodeScanlineUncompressed(in, width); + } + + // check format + byte[] data = new byte[4]; + in.read(data); + if (data[0] != 0x02 || data[1] != 0x02 || (data[2] & 0x80) != 0){ + // not RLE data + decodeScanlineUncompressed(in, width-1); + }else{ + // check scanline width + int readWidth = (data[2] & 0xFF) << 8 | (data[3] & 0xFF); + if (readWidth != width) + throw new IOException("Illegal scanline width in HDR file: "+width+" != "+readWidth); + + // RLE data + decodeScanlineRLE(in, width); + } + } + + public Image load(InputStream in, boolean flipY) throws IOException{ + float gamma = -1f; + float exposure = -1f; + float[] colorcorr = new float[]{ -1f, -1f, -1f }; + + int width = -1, height = -1; + boolean verifiedFormat = false; + + while (true){ + String ln = readString(in); + ln = ln.trim(); + if (ln.startsWith("#") || ln.equals("")){ + if (ln.equals("#?RADIANCE") || ln.equals("#?RGBE")) + verifiedFormat = true; + + continue; // comment or empty statement + } else if (ln.startsWith("+") || ln.startsWith("-")){ + // + or - mark image resolution and start of data + String[] resData = ln.split("\\s"); + if (resData.length != 4){ + throw new IOException("Invalid resolution string in HDR file"); + } + + if (!resData[0].equals("-Y") || !resData[2].equals("+X")){ + logger.warning("Flipping/Rotating attributes ignored!"); + } + + //if (resData[0].endsWith("X")){ + // first width then height + // width = Integer.parseInt(resData[1]); + // height = Integer.parseInt(resData[3]); + //}else{ + width = Integer.parseInt(resData[3]); + height = Integer.parseInt(resData[1]); + //} + + break; + } else { + // regular command + int index = ln.indexOf("="); + if (index < 1){ + logger.log(Level.FINE, "Ignored string: {0}", ln); + continue; + } + + String var = ln.substring(0, index).trim().toLowerCase(); + String value = ln.substring(index+1).trim().toLowerCase(); + if (var.equals("format")){ + if (!value.equals("32-bit_rle_rgbe") && !value.equals("32-bit_rle_xyze")){ + throw new IOException("Unsupported format in HDR picture"); + } + }else if (var.equals("exposure")){ + exposure = Float.parseFloat(value); + }else if (var.equals("gamma")){ + gamma = Float.parseFloat(value); + }else{ + logger.log(Level.WARNING, "HDR Command ignored: {0}", ln); + } + } + } + + assert width != -1 && height != -1; + + if (!verifiedFormat) + logger.warning("Unsure if specified image is Radiance HDR"); + + // some HDR images can get pretty big + System.gc(); + + // each pixel times size of component times # of components + Format pixelFormat; + if (writeRGBE){ + pixelFormat = Format.RGBA8; + }else{ + pixelFormat = Format.RGB16F; + } + + dataStore = BufferUtils.createByteBuffer(width * height * pixelFormat.getBitsPerPixel()); + + int bytesPerPixel = pixelFormat.getBitsPerPixel() / 8; + int scanLineBytes = bytesPerPixel * width; + for (int y = height - 1; y >= 0; y--) { + if (flipY) + dataStore.position(scanLineBytes * y); + + decodeScanline(in, width); + } + in.close(); + + dataStore.rewind(); + return new Image(pixelFormat, width, height, dataStore); + } + + public Object load(AssetInfo info) throws IOException { + if (!(info.getKey() instanceof TextureKey)) + throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey"); + + boolean flip = ((TextureKey) info.getKey()).isFlipY(); + InputStream in = null; + try { + in = info.openStream(); + Image img = load(in, flip); + return img; + } finally { + if (in != null){ + in.close(); + } + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java b/engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java new file mode 100644 index 0000000..53978e9 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009-2010 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; + +import com.jme3.texture.Image; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; + +/** + * ImageFlipper is a utility class used to flip images across the Y axis. + * Due to the standard of where the image origin is between OpenGL and + * other software, this class is required. + * + * @author Kirill Vainer + */ +public class ImageFlipper { + + public static void flipImage(Image img, int index){ + if (img.getFormat().isCompressed()) + throw new UnsupportedOperationException("Flipping compressed " + + "images is unsupported."); + + int w = img.getWidth(); + int h = img.getHeight(); + int halfH = h / 2; + + // bytes per pixel + int bpp = img.getFormat().getBitsPerPixel() / 8; + int scanline = w * bpp; + + ByteBuffer data = img.getData(index); + ByteBuffer temp = BufferUtils.createByteBuffer(scanline); + + data.rewind(); + for (int y = 0; y < halfH; y++){ + int oppY = h - y - 1; + // read in scanline + data.position(y * scanline); + data.limit(data.position() + scanline); + + temp.rewind(); + temp.put(data); + + } + } + +} diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java new file mode 100644 index 0000000..082dc83 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2009-2010 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; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetLoader; +import com.jme3.asset.TextureKey; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.util.BufferUtils; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.logging.Logger; + +public class PFMLoader implements AssetLoader { + + private static final Logger logger = Logger.getLogger(PFMLoader.class.getName()); + + private String readString(InputStream is) throws IOException{ + StringBuilder sb = new StringBuilder(); + while (true){ + int i = is.read(); + if (i == 0x0a || i == -1) // new line or EOF + return sb.toString(); + + sb.append((char)i); + } + } + + private void flipScanline(byte[] scanline){ + for (int i = 0; i < scanline.length; i+=4){ + // flip first and fourth bytes + byte tmp = scanline[i+3]; + scanline[i+3] = scanline[i+0]; + scanline[i+0] = tmp; + + // flip second and third bytes + tmp = scanline[i+2]; + scanline[i+2] = scanline[i+1]; + scanline[i+1] = tmp; + } + } + + private Image load(InputStream in, boolean needYFlip) throws IOException{ + Format format = null; + + String fmtStr = readString(in); + if (fmtStr.equals("PF")){ + format = Format.RGB32F; + }else if (fmtStr.equals("Pf")){ + format = Format.Luminance32F; + }else{ + throw new IOException("File is not PFM format"); + } + + String sizeStr = readString(in); + int spaceIdx = sizeStr.indexOf(" "); + if (spaceIdx <= 0 || spaceIdx >= sizeStr.length() - 1) + throw new IOException("Invalid size syntax in PFM file"); + + int width = Integer.parseInt(sizeStr.substring(0,spaceIdx)); + int height = Integer.parseInt(sizeStr.substring(spaceIdx+1)); + + if (width <= 0 || height <= 0) + throw new IOException("Invalid size specified in PFM file"); + + String scaleStr = readString(in); + float scale = Float.parseFloat(scaleStr); + ByteOrder order = scale < 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + boolean needEndienFlip = order != ByteOrder.nativeOrder(); + + // make sure all unneccessary stuff gets deleted from heap + // before allocating large amount of memory + System.gc(); + + int bytesPerPixel = format.getBitsPerPixel() / 8; + int scanLineBytes = bytesPerPixel * width; + + ByteBuffer imageData = BufferUtils.createByteBuffer(width * height * bytesPerPixel); + byte[] scanline = new byte[width * bytesPerPixel]; + + for (int y = height - 1; y >= 0; y--) { + if (!needYFlip) + imageData.position(scanLineBytes * y); + + int read = 0; + int off = 0; + do { + read = in.read(scanline, off, scanline.length - off); + off += read; + } while (read > 0); + + if (needEndienFlip){ + flipScanline(scanline); + } + + imageData.put(scanline); + } + imageData.rewind(); + + return new Image(format, width, height, imageData); + } + + 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(); + return load(in, ((TextureKey)info.getKey()).isFlipY()); + } finally { + if (in != null){ + in.close(); + } + } + + } + +} diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java new file mode 100644 index 0000000..bbd51d6 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java @@ -0,0 +1,517 @@ +/* + * Copyright (c) 2009-2010 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; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetLoader; +import com.jme3.asset.TextureKey; +import com.jme3.math.FastMath; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.util.BufferUtils; +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * <code>TextureManager</code> provides static methods for building a + * <code>Texture</code> object. Typically, the information supplied is the + * filename and the texture properties. + * + * @author Mark Powell + * @author Joshua Slack - cleaned, commented, added ability to read 16bit true color and color-mapped TGAs. + * @author Kirill Vainer - ported to jME3 + * @version $Id: TGALoader.java 4131 2009-03-19 20:15:28Z blaine.dev $ + */ +public final class TGALoader implements AssetLoader { + + // 0 - no image data in file + public static final int TYPE_NO_IMAGE = 0; + + // 1 - uncompressed, color-mapped image + public static final int TYPE_COLORMAPPED = 1; + + // 2 - uncompressed, true-color image + public static final int TYPE_TRUECOLOR = 2; + + // 3 - uncompressed, black and white image + public static final int TYPE_BLACKANDWHITE = 3; + + // 9 - run-length encoded, color-mapped image + public static final int TYPE_COLORMAPPED_RLE = 9; + + // 10 - run-length encoded, true-color image + public static final int TYPE_TRUECOLOR_RLE = 10; + + // 11 - run-length encoded, black and white image + public static final int TYPE_BLACKANDWHITE_RLE = 11; + + public Object load(AssetInfo info) throws IOException{ + if (!(info.getKey() instanceof TextureKey)) + throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey"); + + boolean flip = ((TextureKey)info.getKey()).isFlipY(); + InputStream in = null; + try { + in = info.openStream(); + Image img = load(in, flip); + return img; + } finally { + if (in != null){ + in.close(); + } + } + } + + /** + * <code>loadImage</code> is a manual image loader which is entirely + * independent of AWT. OUT: RGB888 or RGBA8888 Image object + * + * @return <code>Image</code> object that contains the + * image, either as a RGB888 or RGBA8888 + * @param flip + * Flip the image vertically + * @param exp32 + * Add a dummy Alpha channel to 24b RGB image. + * @param fis + * InputStream of an uncompressed 24b RGB or 32b RGBA TGA + * @throws java.io.IOException + */ + public static Image load(InputStream in, boolean flip) throws IOException { + boolean flipH = false; + + // open a stream to the file + DataInputStream dis = new DataInputStream(new BufferedInputStream(in)); + + // ---------- Start Reading the TGA header ---------- // + // length of the image id (1 byte) + int idLength = dis.readUnsignedByte(); + + // Type of color map (if any) included with the image + // 0 - no color map data is included + // 1 - a color map is included + int colorMapType = dis.readUnsignedByte(); + + // Type of image being read: + int imageType = dis.readUnsignedByte(); + + // Read Color Map Specification (5 bytes) + // Index of first color map entry (if we want to use it, uncomment and remove extra read.) +// short cMapStart = flipEndian(dis.readShort()); + dis.readShort(); + // number of entries in the color map + short cMapLength = flipEndian(dis.readShort()); + // number of bits per color map entry + int cMapDepth = dis.readUnsignedByte(); + + // Read Image Specification (10 bytes) + // horizontal coordinate of lower left corner of image. (if we want to use it, uncomment and remove extra read.) +// int xOffset = flipEndian(dis.readShort()); + dis.readShort(); + // vertical coordinate of lower left corner of image. (if we want to use it, uncomment and remove extra read.) +// int yOffset = flipEndian(dis.readShort()); + dis.readShort(); + // width of image - in pixels + int width = flipEndian(dis.readShort()); + // height of image - in pixels + int height = flipEndian(dis.readShort()); + // bits per pixel in image. + int pixelDepth = dis.readUnsignedByte(); + int imageDescriptor = dis.readUnsignedByte(); + if ((imageDescriptor & 32) != 0) // bit 5 : if 1, flip top/bottom ordering + flip = !flip; + if ((imageDescriptor & 16) != 0) // bit 4 : if 1, flip left/right ordering + flipH = !flipH; + + // ---------- Done Reading the TGA header ---------- // + + // Skip image ID + if (idLength > 0) + in.skip(idLength); + + ColorMapEntry[] cMapEntries = null; + if (colorMapType != 0) { + // read the color map. + int bytesInColorMap = (cMapDepth * cMapLength) >> 3; + int bitsPerColor = Math.min(cMapDepth/3 , 8); + + byte[] cMapData = new byte[bytesInColorMap]; + in.read(cMapData); + + // Only go to the trouble of constructing the color map + // table if this is declared a color mapped image. + if (imageType == TYPE_COLORMAPPED || imageType == TYPE_COLORMAPPED_RLE) { + cMapEntries = new ColorMapEntry[cMapLength]; + int alphaSize = cMapDepth - (3*bitsPerColor); + float scalar = 255f / (FastMath.pow(2, bitsPerColor) - 1); + float alphaScalar = 255f / (FastMath.pow(2, alphaSize) - 1); + for (int i = 0; i < cMapLength; i++) { + ColorMapEntry entry = new ColorMapEntry(); + int offset = cMapDepth * i; + entry.red = (byte)(int)(getBitsAsByte(cMapData, offset, bitsPerColor) * scalar); + entry.green = (byte)(int)(getBitsAsByte(cMapData, offset+bitsPerColor, bitsPerColor) * scalar); + entry.blue = (byte)(int)(getBitsAsByte(cMapData, offset+(2*bitsPerColor), bitsPerColor) * scalar); + if (alphaSize <= 0) + entry.alpha = (byte)255; + else + entry.alpha = (byte)(int)(getBitsAsByte(cMapData, offset+(3*bitsPerColor), alphaSize) * alphaScalar); + + cMapEntries[i] = entry; + } + } + } + + + // Allocate image data array + Format format; + byte[] rawData = null; + int dl; + if (pixelDepth == 32) { + rawData = new byte[width * height * 4]; + dl = 4; + } else { + rawData = new byte[width * height * 3]; + dl = 3; + } + int rawDataIndex = 0; + + if (imageType == TYPE_TRUECOLOR) { + byte red = 0; + byte green = 0; + byte blue = 0; + byte alpha = 0; + + // Faster than doing a 16-or-24-or-32 check on each individual pixel, + // just make a seperate loop for each. + if (pixelDepth == 16) { + byte[] data = new byte[2]; + float scalar = 255f/31f; + for (int i = 0; i <= (height - 1); i++) { + if (!flip) + rawDataIndex = (height - 1 - i) * width * dl; + for (int j = 0; j < width; j++) { + data[1] = dis.readByte(); + data[0] = dis.readByte(); + rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 1, 5) * scalar); + rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 6, 5) * scalar); + rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 11, 5) * scalar); + if (dl == 4) { + // create an alpha channel + alpha = getBitsAsByte(data, 0, 1); + if (alpha == 1) alpha = (byte)255; + rawData[rawDataIndex++] = alpha; + } + } + } + + format = dl == 4 ? Format.RGBA8 : Format.RGB8; + } else if (pixelDepth == 24){ + for (int y = 0; y < height; y++) { + if (!flip) + rawDataIndex = (height - 1 - y) * width * dl; + else + rawDataIndex = y * width * dl; + + dis.readFully(rawData, rawDataIndex, width * dl); +// for (int x = 0; x < width; x++) { + //read scanline +// blue = dis.readByte(); +// green = dis.readByte(); +// red = dis.readByte(); +// rawData[rawDataIndex++] = red; +// rawData[rawDataIndex++] = green; +// rawData[rawDataIndex++] = blue; +// } + } + format = Format.BGR8; + } else if (pixelDepth == 32){ + for (int i = 0; i <= (height - 1); i++) { + if (!flip) + rawDataIndex = (height - 1 - i) * width * dl; + + for (int j = 0; j < width; j++) { + blue = dis.readByte(); + green = dis.readByte(); + red = dis.readByte(); + alpha = dis.readByte(); + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + rawData[rawDataIndex++] = alpha; + } + } + format = Format.RGBA8; + }else{ + throw new IOException("Unsupported TGA true color depth: "+pixelDepth); + } + } else if( imageType == TYPE_TRUECOLOR_RLE ) { + byte red = 0; + byte green = 0; + byte blue = 0; + byte alpha = 0; + // Faster than doing a 16-or-24-or-32 check on each individual pixel, + // just make a seperate loop for each. + if( pixelDepth == 32 ){ + for( int i = 0; i <= ( height - 1 ); ++i ){ + if( !flip ){ + rawDataIndex = ( height - 1 - i ) * width * dl; + } + + for( int j = 0; j < width; ++j ){ + // Get the number of pixels the next chunk covers (either packed or unpacked) + int count = dis.readByte(); + if( ( count & 0x80 ) != 0 ){ + // Its an RLE packed block - use the following 1 pixel for the next <count> pixels + count &= 0x07f; + j += count; + blue = dis.readByte(); + green = dis.readByte(); + red = dis.readByte(); + alpha = dis.readByte(); + while( count-- >= 0 ){ + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + rawData[rawDataIndex++] = alpha; + } + } else{ + // Its not RLE packed, but the next <count> pixels are raw. + j += count; + while( count-- >= 0 ){ + blue = dis.readByte(); + green = dis.readByte(); + red = dis.readByte(); + alpha = dis.readByte(); + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + rawData[rawDataIndex++] = alpha; + } + } + } + } + format = Format.RGBA8; + } else if( pixelDepth == 24 ){ + for( int i = 0; i <= ( height - 1 ); i++ ){ + if( !flip ){ + rawDataIndex = ( height - 1 - i ) * width * dl; + } + for( int j = 0; j < width; ++j ){ + // Get the number of pixels the next chunk covers (either packed or unpacked) + int count = dis.readByte(); + if( ( count & 0x80 ) != 0 ){ + // Its an RLE packed block - use the following 1 pixel for the next <count> pixels + count &= 0x07f; + j += count; + blue = dis.readByte(); + green = dis.readByte(); + red = dis.readByte(); + while( count-- >= 0 ){ + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + } + } else{ + // Its not RLE packed, but the next <count> pixels are raw. + j += count; + while( count-- >= 0 ){ + blue = dis.readByte(); + green = dis.readByte(); + red = dis.readByte(); + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + } + } + } + } + format = Format.RGB8; + } else if( pixelDepth == 16 ){ + byte[] data = new byte[ 2 ]; + float scalar = 255f / 31f; + for( int i = 0; i <= ( height - 1 ); i++ ){ + if( !flip ){ + rawDataIndex = ( height - 1 - i ) * width * dl; + } + for( int j = 0; j < width; j++ ){ + // Get the number of pixels the next chunk covers (either packed or unpacked) + int count = dis.readByte(); + if( ( count & 0x80 ) != 0 ){ + // Its an RLE packed block - use the following 1 pixel for the next <count> pixels + count &= 0x07f; + j += count; + data[1] = dis.readByte(); + data[0] = dis.readByte(); + blue = (byte) (int) ( getBitsAsByte( data, 1, 5 ) * scalar ); + green = (byte) (int) ( getBitsAsByte( data, 6, 5 ) * scalar ); + red = (byte) (int) ( getBitsAsByte( data, 11, 5 ) * scalar ); + while( count-- >= 0 ){ + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + } + } else{ + // Its not RLE packed, but the next <count> pixels are raw. + j += count; + while( count-- >= 0 ){ + data[1] = dis.readByte(); + data[0] = dis.readByte(); + blue = (byte) (int) ( getBitsAsByte( data, 1, 5 ) * scalar ); + green = (byte) (int) ( getBitsAsByte( data, 6, 5 ) * scalar ); + red = (byte) (int) ( getBitsAsByte( data, 11, 5 ) * scalar ); + rawData[rawDataIndex++] = red; + rawData[rawDataIndex++] = green; + rawData[rawDataIndex++] = blue; + } + } + } + } + format = Format.RGB8; + } else{ + throw new IOException( "Unsupported TGA true color depth: " + pixelDepth ); + } + + } else if( imageType == TYPE_COLORMAPPED ){ + int bytesPerIndex = pixelDepth / 8; + + if (bytesPerIndex == 1) { + for (int i = 0; i <= (height - 1); i++) { + if (!flip) + rawDataIndex = (height - 1 - i) * width * dl; + for (int j = 0; j < width; j++) { + int index = dis.readUnsignedByte(); + if (index >= cMapEntries.length || index < 0) + throw new IOException("TGA: Invalid color map entry referenced: "+index); + + ColorMapEntry entry = cMapEntries[index]; + rawData[rawDataIndex++] = entry.red; + rawData[rawDataIndex++] = entry.green; + rawData[rawDataIndex++] = entry.blue; + if (dl == 4) { + rawData[rawDataIndex++] = entry.alpha; + } + + } + } + } else if (bytesPerIndex == 2) { + for (int i = 0; i <= (height - 1); i++) { + if (!flip) + rawDataIndex = (height - 1 - i) * width * dl; + for (int j = 0; j < width; j++) { + int index = flipEndian(dis.readShort()); + if (index >= cMapEntries.length || index < 0) + throw new IOException("TGA: Invalid color map entry referenced: "+index); + + ColorMapEntry entry = cMapEntries[index]; + rawData[rawDataIndex++] = entry.red; + rawData[rawDataIndex++] = entry.green; + rawData[rawDataIndex++] = entry.blue; + if (dl == 4) { + rawData[rawDataIndex++] = entry.alpha; + } + } + } + } else { + throw new IOException("TGA: unknown colormap indexing size used: "+bytesPerIndex); + } + + format = dl == 4 ? Format.RGBA8 : Format.RGB8; + } else { + throw new IOException("Grayscale TGA not supported"); + } + + + in.close(); + // Get a pointer to the image memory + ByteBuffer scratch = BufferUtils.createByteBuffer(rawData.length); + scratch.clear(); + scratch.put(rawData); + scratch.rewind(); + // Create the Image object + Image textureImage = new Image(); + textureImage.setFormat(format); + textureImage.setWidth(width); + textureImage.setHeight(height); + textureImage.setData(scratch); + return textureImage; + } + + private static byte getBitsAsByte(byte[] data, int offset, int length) { + int offsetBytes = offset / 8; + int indexBits = offset % 8; + int rVal = 0; + + // start at data[offsetBytes]... spill into next byte as needed. + for (int i = length; --i >=0;) { + byte b = data[offsetBytes]; + int test = indexBits == 7 ? 1 : 2 << (6-indexBits); + if ((b & test) != 0) { + if (i == 0) + rVal++; + else + rVal += (2 << i-1); + } + indexBits++; + if (indexBits == 8) { + indexBits = 0; + offsetBytes++; + } + } + + return (byte)rVal; + } + + /** + * <code>flipEndian</code> is used to flip the endian bit of the header + * file. + * + * @param signedShort + * the bit to flip. + * @return the flipped bit. + */ + private static short flipEndian(short signedShort) { + int input = signedShort & 0xFFFF; + return (short) (input << 8 | (input & 0xFF00) >>> 8); + } + + static class ColorMapEntry { + byte red, green, blue, alpha; + + @Override + public String toString() { + return "entry: "+red+","+green+","+blue+","+alpha; + } + } +} |