1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.jme3.texture.plugins; 34 35 import com.jme3.asset.AssetInfo; 36 import com.jme3.asset.AssetLoader; 37 import com.jme3.asset.TextureKey; 38 import com.jme3.math.FastMath; 39 import com.jme3.texture.Image; 40 import com.jme3.texture.Image.Format; 41 import com.jme3.util.BufferUtils; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.nio.ByteBuffer; 45 import java.util.logging.Level; 46 import java.util.logging.Logger; 47 48 public class HDRLoader implements AssetLoader { 49 50 private static final Logger logger = Logger.getLogger(HDRLoader.class.getName()); 51 52 private boolean writeRGBE = false; 53 private ByteBuffer rleTempBuffer; 54 private ByteBuffer dataStore; 55 private final float[] tempF = new float[3]; 56 HDRLoader(boolean writeRGBE)57 public HDRLoader(boolean writeRGBE){ 58 this.writeRGBE = writeRGBE; 59 } 60 HDRLoader()61 public HDRLoader(){ 62 } 63 convertFloatToRGBE(byte[] rgbe, float red, float green, float blue)64 public static void convertFloatToRGBE(byte[] rgbe, float red, float green, float blue){ 65 double max = red; 66 if (green > max) max = green; 67 if (blue > max) max = blue; 68 if (max < 1.0e-32){ 69 rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; 70 }else{ 71 double exp = Math.ceil( Math.log10(max) / Math.log10(2) ); 72 double divider = Math.pow(2.0, exp); 73 rgbe[0] = (byte) ((red / divider) * 255.0); 74 rgbe[1] = (byte) ((green / divider) * 255.0); 75 rgbe[2] = (byte) ((blue / divider) * 255.0); 76 rgbe[3] = (byte) (exp + 128.0); 77 } 78 } 79 convertRGBEtoFloat(byte[] rgbe, float[] rgbf)80 public static void convertRGBEtoFloat(byte[] rgbe, float[] rgbf){ 81 int R = rgbe[0] & 0xFF, 82 G = rgbe[1] & 0xFF, 83 B = rgbe[2] & 0xFF, 84 E = rgbe[3] & 0xFF; 85 86 float e = (float) Math.pow(2f, E - (128 + 8) ); 87 rgbf[0] = R * e; 88 rgbf[1] = G * e; 89 rgbf[2] = B * e; 90 } 91 convertRGBEtoFloat2(byte[] rgbe, float[] rgbf)92 public static void convertRGBEtoFloat2(byte[] rgbe, float[] rgbf){ 93 int R = rgbe[0] & 0xFF, 94 G = rgbe[1] & 0xFF, 95 B = rgbe[2] & 0xFF, 96 E = rgbe[3] & 0xFF; 97 98 float e = (float) Math.pow(2f, E - 128); 99 rgbf[0] = (R / 256.0f) * e; 100 rgbf[1] = (G / 256.0f) * e; 101 rgbf[2] = (B / 256.0f) * e; 102 } 103 convertRGBEtoFloat3(byte[] rgbe, float[] rgbf)104 public static void convertRGBEtoFloat3(byte[] rgbe, float[] rgbf){ 105 int R = rgbe[0] & 0xFF, 106 G = rgbe[1] & 0xFF, 107 B = rgbe[2] & 0xFF, 108 E = rgbe[3] & 0xFF; 109 110 float e = (float) Math.pow(2f, E - (128 + 8) ); 111 rgbf[0] = R * e; 112 rgbf[1] = G * e; 113 rgbf[2] = B * e; 114 } 115 flip(int in)116 private short flip(int in){ 117 return (short) ((in << 8 & 0xFF00) | (in >> 8)); 118 } 119 writeRGBE(byte[] rgbe)120 private void writeRGBE(byte[] rgbe){ 121 if (writeRGBE){ 122 dataStore.put(rgbe); 123 }else{ 124 convertRGBEtoFloat(rgbe, tempF); 125 dataStore.putShort(FastMath.convertFloatToHalf(tempF[0])) 126 .putShort(FastMath.convertFloatToHalf(tempF[1])). 127 putShort(FastMath.convertFloatToHalf(tempF[2])); 128 } 129 } 130 readString(InputStream is)131 private String readString(InputStream is) throws IOException{ 132 StringBuilder sb = new StringBuilder(); 133 while (true){ 134 int i = is.read(); 135 if (i == 0x0a || i == -1) // new line or EOF 136 return sb.toString(); 137 138 sb.append((char)i); 139 } 140 } 141 decodeScanlineRLE(InputStream in, int width)142 private boolean decodeScanlineRLE(InputStream in, int width) throws IOException{ 143 // must deocde RLE data into temp buffer before converting to float 144 if (rleTempBuffer == null){ 145 rleTempBuffer = BufferUtils.createByteBuffer(width * 4); 146 }else{ 147 rleTempBuffer.clear(); 148 if (rleTempBuffer.remaining() < width * 4) 149 rleTempBuffer = BufferUtils.createByteBuffer(width * 4); 150 } 151 152 // read each component seperately 153 for (int i = 0; i < 4; i++) { 154 // read WIDTH bytes for the channel 155 for (int j = 0; j < width;) { 156 int code = in.read(); 157 if (code > 128) { // run 158 code -= 128; 159 int val = in.read(); 160 while ((code--) != 0) { 161 rleTempBuffer.put( (j++) * 4 + i , (byte)val); 162 //scanline[j++][i] = val; 163 } 164 } else { // non-run 165 while ((code--) != 0) { 166 int val = in.read(); 167 rleTempBuffer.put( (j++) * 4 + i, (byte)val); 168 //scanline[j++][i] = in.read(); 169 } 170 } 171 } 172 } 173 174 rleTempBuffer.rewind(); 175 byte[] rgbe = new byte[4]; 176 // float[] temp = new float[3]; 177 178 // decode temp buffer into float data 179 for (int i = 0; i < width; i++){ 180 rleTempBuffer.get(rgbe); 181 writeRGBE(rgbe); 182 } 183 184 return true; 185 } 186 decodeScanlineUncompressed(InputStream in, int width)187 private boolean decodeScanlineUncompressed(InputStream in, int width) throws IOException{ 188 byte[] rgbe = new byte[4]; 189 190 for (int i = 0; i < width; i+=3){ 191 if (in.read(rgbe) < 1) 192 return false; 193 194 writeRGBE(rgbe); 195 } 196 return true; 197 } 198 decodeScanline(InputStream in, int width)199 private void decodeScanline(InputStream in, int width) throws IOException{ 200 if (width < 8 || width > 0x7fff){ 201 // too short/long for RLE compression 202 decodeScanlineUncompressed(in, width); 203 } 204 205 // check format 206 byte[] data = new byte[4]; 207 in.read(data); 208 if (data[0] != 0x02 || data[1] != 0x02 || (data[2] & 0x80) != 0){ 209 // not RLE data 210 decodeScanlineUncompressed(in, width-1); 211 }else{ 212 // check scanline width 213 int readWidth = (data[2] & 0xFF) << 8 | (data[3] & 0xFF); 214 if (readWidth != width) 215 throw new IOException("Illegal scanline width in HDR file: "+width+" != "+readWidth); 216 217 // RLE data 218 decodeScanlineRLE(in, width); 219 } 220 } 221 load(InputStream in, boolean flipY)222 public Image load(InputStream in, boolean flipY) throws IOException{ 223 float gamma = -1f; 224 float exposure = -1f; 225 float[] colorcorr = new float[]{ -1f, -1f, -1f }; 226 227 int width = -1, height = -1; 228 boolean verifiedFormat = false; 229 230 while (true){ 231 String ln = readString(in); 232 ln = ln.trim(); 233 if (ln.startsWith("#") || ln.equals("")){ 234 if (ln.equals("#?RADIANCE") || ln.equals("#?RGBE")) 235 verifiedFormat = true; 236 237 continue; // comment or empty statement 238 } else if (ln.startsWith("+") || ln.startsWith("-")){ 239 // + or - mark image resolution and start of data 240 String[] resData = ln.split("\\s"); 241 if (resData.length != 4){ 242 throw new IOException("Invalid resolution string in HDR file"); 243 } 244 245 if (!resData[0].equals("-Y") || !resData[2].equals("+X")){ 246 logger.warning("Flipping/Rotating attributes ignored!"); 247 } 248 249 //if (resData[0].endsWith("X")){ 250 // first width then height 251 // width = Integer.parseInt(resData[1]); 252 // height = Integer.parseInt(resData[3]); 253 //}else{ 254 width = Integer.parseInt(resData[3]); 255 height = Integer.parseInt(resData[1]); 256 //} 257 258 break; 259 } else { 260 // regular command 261 int index = ln.indexOf("="); 262 if (index < 1){ 263 logger.log(Level.FINE, "Ignored string: {0}", ln); 264 continue; 265 } 266 267 String var = ln.substring(0, index).trim().toLowerCase(); 268 String value = ln.substring(index+1).trim().toLowerCase(); 269 if (var.equals("format")){ 270 if (!value.equals("32-bit_rle_rgbe") && !value.equals("32-bit_rle_xyze")){ 271 throw new IOException("Unsupported format in HDR picture"); 272 } 273 }else if (var.equals("exposure")){ 274 exposure = Float.parseFloat(value); 275 }else if (var.equals("gamma")){ 276 gamma = Float.parseFloat(value); 277 }else{ 278 logger.log(Level.WARNING, "HDR Command ignored: {0}", ln); 279 } 280 } 281 } 282 283 assert width != -1 && height != -1; 284 285 if (!verifiedFormat) 286 logger.warning("Unsure if specified image is Radiance HDR"); 287 288 // some HDR images can get pretty big 289 System.gc(); 290 291 // each pixel times size of component times # of components 292 Format pixelFormat; 293 if (writeRGBE){ 294 pixelFormat = Format.RGBA8; 295 }else{ 296 pixelFormat = Format.RGB16F; 297 } 298 299 dataStore = BufferUtils.createByteBuffer(width * height * pixelFormat.getBitsPerPixel()); 300 301 int bytesPerPixel = pixelFormat.getBitsPerPixel() / 8; 302 int scanLineBytes = bytesPerPixel * width; 303 for (int y = height - 1; y >= 0; y--) { 304 if (flipY) 305 dataStore.position(scanLineBytes * y); 306 307 decodeScanline(in, width); 308 } 309 in.close(); 310 311 dataStore.rewind(); 312 return new Image(pixelFormat, width, height, dataStore); 313 } 314 load(AssetInfo info)315 public Object load(AssetInfo info) throws IOException { 316 if (!(info.getKey() instanceof TextureKey)) 317 throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey"); 318 319 boolean flip = ((TextureKey) info.getKey()).isFlipY(); 320 InputStream in = null; 321 try { 322 in = info.openStream(); 323 Image img = load(in, flip); 324 return img; 325 } finally { 326 if (in != null){ 327 in.close(); 328 } 329 } 330 } 331 332 } 333