Commits

Michael Ludwig committed 8bf2259

Clean up existing texture loaders and implement an HDR file loader.

Comments (0)

Files changed (6)

ferox-renderer/src/main/java/com/ferox/renderer/loader/DDSImageFileLoader.java

 import com.ferox.renderer.Sampler;
 import com.ferox.renderer.builder.Builder;
 
+import java.io.BufferedInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 
 /**
  * An implementation of ImageFileLoader that relies on DDSTexture to load .dds files.
  */
 public class DDSImageFileLoader implements ImageFileLoader {
     @Override
-    public Builder<? extends Sampler> read(Framework framework, InputStream stream) throws IOException {
+    public Builder<? extends Sampler> read(Framework framework, BufferedInputStream stream)
+            throws IOException {
         if (DDSTexture.isDDSTexture(stream)) {
             return DDSTexture.readTexture(framework, stream);
         } else {

ferox-renderer/src/main/java/com/ferox/renderer/loader/DDSTexture.java

      * @throws IOException if an IOException occurs while reading, or if the stream is an invalid or
      *                     unsupported DDS texture
      */
-    public static Builder<? extends Sampler> readTexture(Framework framework, InputStream stream)
+    public static Builder<? extends Sampler> readTexture(Framework framework, BufferedInputStream stream)
             throws IOException {
         if (stream == null) {
             throw new IOException("Cannot read a texture from a null stream");
      *
      * @throws NullPointerException if stream is null
      */
-    public static boolean isDDSTexture(InputStream stream) {
+    public static boolean isDDSTexture(BufferedInputStream stream) {
         if (stream == null) {
             throw new NullPointerException("Cannot test a null stream");
         }
 
-        if (!(stream instanceof BufferedInputStream)) {
-            stream = new BufferedInputStream(stream); // this way marking is supported
-        }
         try {
             DDSHeader header;
             try {
      * Also keeps track of byte size of the glType primitive and the number of
      * primitives required to store a color element.
      */
+    // FIXME handle sRGB correctly somehow
     private static enum DXGIPixelFormat {
         DXGI_FORMAT_UNKNOWN,
         DXGI_FORMAT_R32G32B32A32_TYPELESS,
         DXGI_FORMAT_R8G8_B8G8_UNORM,
         DXGI_FORMAT_G8R8_G8B8_UNORM,
         // DXT1
-        DXGI_FORMAT_BC1_TYPELESS(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGB),
+        DXGI_FORMAT_BC1_TYPELESS(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGB),
         // DXT1
-        DXGI_FORMAT_BC1_UNORM(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGB),
+        DXGI_FORMAT_BC1_UNORM(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGB),
         // DXT1
-        DXGI_FORMAT_BC1_UNORM_SRGB(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGB),
+        DXGI_FORMAT_BC1_UNORM_SRGB(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGB),
         // DXT3
-        DXGI_FORMAT_BC2_TYPELESS(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
+        DXGI_FORMAT_BC2_TYPELESS(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
         // DXT3
-        DXGI_FORMAT_BC2_UNORM(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
+        DXGI_FORMAT_BC2_UNORM(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
         // DXT3
-        DXGI_FORMAT_BC2_UNORM_SRGB(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
+        DXGI_FORMAT_BC2_UNORM_SRGB(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
         // DXT5
-        DXGI_FORMAT_BC3_TYPELESS(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
+        DXGI_FORMAT_BC3_TYPELESS(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
         // DXT5
-        DXGI_FORMAT_BC3_UNORM(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
+        DXGI_FORMAT_BC3_UNORM(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
         // DXT5
-        DXGI_FORMAT_BC3_UNORM_SRGB(DataType.UNSIGNED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
+        DXGI_FORMAT_BC3_UNORM_SRGB(DataType.UNSIGNED_NORMALIZED_BYTE, Sampler.TexelFormat.COMPRESSED_RGBA),
         DXGI_FORMAT_BC4_TYPELESS,
         DXGI_FORMAT_BC4_UNORM,
         DXGI_FORMAT_BC4_SNORM,
         try {
             Class<?> arrayType = data.getClass();
             Method m = layer.getClass().getMethod(methodName, arrayType);
+            m.setAccessible(true);
             m.invoke(layer, data);
         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
             throw new RuntimeException("Bug in format method selection", e);
             case DXGI_FORMAT_BC1_UNORM:
             case DXGI_FORMAT_BC1_UNORM_SRGB:
                 // DXT1 buffer size
-                return (int) (8 * Math.ceil(w / 4.0) * Math.ceil(4.0));
+                return (int) (8 * Math.ceil(w / 4.0) * Math.ceil(h / 4.0));
             case DXGI_FORMAT_BC2_TYPELESS:
             case DXGI_FORMAT_BC2_UNORM:
             case DXGI_FORMAT_BC2_UNORM_SRGB:
             case DXGI_FORMAT_BC3_UNORM:
             case DXGI_FORMAT_BC3_UNORM_SRGB:
                 // DXT3 and DXT5 buffer size
-                return (int) (16 * Math.ceil(w / 4.0) * Math.ceil(4.0));
+                return (int) (16 * Math.ceil(w / 4.0) * Math.ceil(h / 4.0));
             default:
                 return format.format.getComponentCount() * format.type.getByteCount() * w * h * d;
             }

ferox-renderer/src/main/java/com/ferox/renderer/loader/ImageFileLoader.java

 import com.ferox.renderer.Sampler;
 import com.ferox.renderer.builder.Builder;
 
+import java.io.BufferedInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 
 /**
  * <p/>
      *
      * @throws IOException if there are any problems reading the texture
      */
-    public Builder<? extends Sampler> read(Framework framework, InputStream stream) throws IOException;
+    public Builder<? extends Sampler> read(Framework framework, BufferedInputStream stream)
+            throws IOException;
 }

ferox-renderer/src/main/java/com/ferox/renderer/loader/ImageIOImageFileLoader.java

 
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 
 /**
  * An ImageFileLoader that uses ImageIO to load files in gif, png, or jpg files (this depends on the ImageIO
  */
 public class ImageIOImageFileLoader implements ImageFileLoader {
     @Override
-    public Builder<? extends Texture> read(Framework framework, InputStream stream) throws IOException {
+    public Builder<? extends Texture> read(Framework framework, BufferedInputStream stream)
+            throws IOException {
         // I'm assuming that read() will restore the stream's position
         // if no reader is found
 

ferox-renderer/src/main/java/com/ferox/renderer/loader/RadianceImageLoader.java

+package com.ferox.renderer.loader;
+
+import com.ferox.renderer.Framework;
+import com.ferox.renderer.Texture2D;
+import com.ferox.renderer.builder.Builder;
+import com.ferox.renderer.builder.Texture2DBuilder;
+import com.ferox.renderer.builder.TextureBuilder;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *
+ */
+public class RadianceImageLoader implements ImageFileLoader {
+    private static final Pattern COMMON_RES = Pattern.compile("-Y (\\d+) \\+X (\\d+)");
+    private static final Pattern ANY_RES = Pattern.compile("[-\\+]Y \\d+ [-\\+]X \\d+");
+
+    @Override
+    public Builder<Texture2D> read(Framework framework, BufferedInputStream stream) throws IOException {
+        stream.mark(128);
+        if (!processMagicNumber(stream)) {
+            stream.reset();
+            return null;
+        }
+
+        Map<String, String> vars = processVariables(stream);
+        if (!"32-bit_rle_rgbe".equals(vars.get("FORMAT"))) {
+            throw new IOException("Format must be 32-bit-rle-rgbe, not: " + vars.get("FORMAT"));
+        }
+
+        int width = Integer.parseInt(vars.get("WIDTH"));
+        int height = Integer.parseInt(vars.get("HEIGHT"));
+        Texture2DBuilder builder = framework.newTexture2D().width(width).height(height).interpolated();
+        TextureBuilder.CompressedRGBData data = builder.rgb().mipmap(0);
+        data.from(readImage(width, height, true, true, stream));
+        return builder;
+    }
+
+    private float[] readImage(int width, int height, boolean topToBottom, boolean leftToRight, InputStream in)
+            throws IOException {
+        byte[] scan = new byte[width * 4];
+        float[] img = new float[width * height * 3];
+        for (int y = 0; y < height; y++) {
+            // we're using OpenGL's coordinate frame where bottom is 0
+            readScanLine(img, (topToBottom ? height - y - 1 : y), width, leftToRight, scan, in);
+        }
+        return img;
+    }
+
+    private void readScanLine(float[] image, int imgY, int imgWidth, boolean leftToRight, byte[] scan,
+                              InputStream in) throws IOException {
+        int baseOffset = 3 * imgY * imgWidth;
+
+        Arrays.fill(scan, (byte) 0);
+        // read the first pixel
+        readAll(in, scan, 0, 4);
+        boolean rle = true;
+        if (imgWidth < 8 || imgWidth > 0x7fff) {
+            rle = false; // image is too small
+        }
+        if (scan[0] != 2 || scan[1] != 2 || (scan[2] & 0x80) != 0) {
+            rle = false; // not an rle scanline
+        }
+
+        if (rle) {
+            int scanWidth = (scan[2] << 8) | scan[3];
+            if (scanWidth != imgWidth) {
+                throw new IOException("Wrong scanline width: " + scanWidth);
+            }
+
+            // read each channel of the RGBE data into scan
+            int p = 0;
+            for (int i = 0; i < 4; i++) {
+                while (p < (i + 1) * imgWidth) {
+                    int len = in.read();
+                    if (len > 128) {
+                        len = len - 128;
+                        // run of same value (which is the next byte)
+                        Arrays.fill(scan, p, p + len, (byte) in.read());
+                        p += len;
+                    } else {
+                        // dump of channel
+                        readAll(in, scan, p, len);
+                        p += len;
+                    }
+                }
+            }
+
+
+            //            System.out.println(Arrays.toString(scan));
+
+            // interpret the channels into pixels
+            byte[] pixel = new byte[4];
+            for (int x = 0; x < imgWidth; x++) {
+                pixel[0] = scan[x];
+                pixel[1] = scan[x + imgWidth];
+                pixel[2] = scan[x + 2 * imgWidth];
+                pixel[3] = scan[x + 3 * imgWidth];
+
+                int xOffset = (leftToRight ? 3 * x : 3 * (imgWidth - x - 1));
+                convertRGBE(image, baseOffset + xOffset, pixel, 0);
+            }
+            //            System.out.println();
+        } else {
+            // scanline is flat so read it fully
+            readAll(in, scan, 4);
+            for (int x = 0; x < imgWidth; x++) {
+                int xOffset = (leftToRight ? 3 * x : 3 * (imgWidth - x - 1));
+                convertRGBE(image, baseOffset + xOffset, scan, x * 4);
+            }
+        }
+    }
+
+    // TODO This doesn't work
+    // FIXME test case: create a simple PNG image that's like 10 pixels across
+    // and with a well-defined color, convert it to HDR and then load it in and see what happens
+    private void convertRGBE(float[] image, int imgOffset, byte[] rgbe, int offset) {
+        if (rgbe[offset + 3] != 0) {
+            // real pixel
+            //            float v = (float) Math.pow(1.0 / 256.0, rgbe[offset + 3]);
+            //            float v = (1 << rgbe[offset + 3]) / 256.0f;
+            //            int e = (0xff & rgbe[offset + 3]);
+            float v = (float) Math.pow(2.0, (0xff & rgbe[offset + 3]) - 128) / 256f;
+            //            float v = (float) (1.0 * Math.pow(2, (int) (rgbe[offset + 3]) - 8));
+            // these are meant to be unsigned bytes
+            image[imgOffset] = v * ((0xff & rgbe[offset]));
+            image[imgOffset + 1] = v * ((0xff & rgbe[offset + 1]));
+            image[imgOffset + 2] = v * ((0xff & rgbe[offset + 2]));
+        } else {
+            // black
+            image[imgOffset] = 0f;
+            image[imgOffset + 1] = 0f;
+            image[imgOffset + 2] = 0f;
+        }
+    }
+
+    private boolean processMagicNumber(InputStream in) throws IOException {
+        // FIXME if we have fewer than this many bytes, we'll throw an exception and that breaks the whole thing
+        byte[] magic = new byte[6];
+        readAll(in, magic);
+        if (magic[0] != '#' || magic[1] != '?') {
+            return false;
+        }
+
+        if (magic[2] == 'R' && magic[3] == 'G' && magic[4] == 'B' && magic[5] == 'E') {
+            return true;
+        }
+        // else read a few more bytes and check for RADIANCE
+        if (magic[2] == 'R' && magic[3] == 'A' && magic[4] == 'D' && magic[5] == 'I') {
+            magic = new byte[4];
+            readAll(in, magic);
+            if (magic[0] == 'A' && magic[1] == 'N' && magic[2] == 'C' && magic[3] == 'E') {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private Map<String, String> processVariables(InputStream in) throws IOException {
+        Map<String, String> vars = new HashMap<>();
+
+        StringBuilder sb = new StringBuilder();
+        int b;
+        while ((b = in.read()) >= 0) {
+            if (b == '\n') {
+                // move onto next variable
+                String line = sb.toString().trim();
+                if (!line.isEmpty()) {
+                    // check for variable pattern
+                    int equals = line.indexOf('=');
+                    if (equals >= 0) {
+                        vars.put(line.substring(0, equals), line.substring(equals + 1));
+                    } else {
+                        // check for resolution line
+                        Matcher m = COMMON_RES.matcher(line);
+                        if (m.matches()) {
+                            vars.put("WIDTH", m.group(2));
+                            vars.put("HEIGHT", m.group(1));
+                            return vars;
+                        } else if (ANY_RES.matcher(line).matches()) {
+                            throw new IOException("Resolution other than -Y <H> +X <W> is not supported: " +
+                                                  line);
+                        } else if (!line.startsWith("#")) {
+                            throw new IOException("Unexpected header line: " + line);
+                        }
+                    }
+                }
+                // empty line at end of header, but we just skip it since we process the dimensions
+                // as part of this method, too
+
+                // regardless, reset buffer
+                sb.setLength(0);
+            } else {
+                sb.append((char) b);
+            }
+        }
+        throw new IOException("Did not encounter resolution specification");
+    }
+
+    private static void readAll(InputStream in, byte[] array) throws IOException {
+        readAll(in, array, 0, array.length);
+    }
+
+    private static void readAll(InputStream in, byte[] array, int offset) throws IOException {
+        readAll(in, array, offset, array.length - offset);
+    }
+
+    // read bytes from the given stream until the array has filled with length
+    // fails if the end-of-stream happens before length has been read
+    private static void readAll(InputStream in, byte[] array, int offset, int length) throws IOException {
+        int remaining = length;
+        int read;
+        while (remaining > 0) {
+            read = in.read(array, offset, remaining);
+            if (read < 0) {
+                throw new IOException("Unexpected end of stream");
+            }
+            offset += read;
+            remaining -= read;
+        }
+    }
+}

ferox-renderer/src/main/java/com/ferox/renderer/loader/TextureLoader.java

     static {
         registerLoader(new ImageIOImageFileLoader());
         registerLoader(new DDSImageFileLoader());
+        registerLoader(new RadianceImageLoader());
     }
 
     private TextureLoader() {
      *
      * @throws IOException if the file can't be read, if it's unsupported, etc.
      */
+    // FIXME returning builders does not let you configure the texture parameters, we need a better option
+    // consider the geometry approach, where we don't load directly to the GPU resource?
     public static Builder<? extends Sampler> readTexture(Framework framework, File file) throws IOException {
         if (file == null) {
             throw new IOException("Cannot load a texture image from a null file");
     public static Builder<? extends Sampler> readTexture(Framework framework, InputStream stream)
             throws IOException {
         // make sure we're buffered
-        if (!(stream instanceof BufferedInputStream)) {
-            stream = new BufferedInputStream(stream);
+        BufferedInputStream in;
+        if (stream instanceof BufferedInputStream) {
+            in = (BufferedInputStream) stream;
+        } else {
+            in = new BufferedInputStream(stream);
         }
 
         // load the file
 
         synchronized (loaders) {
             for (int i = loaders.size() - 1; i >= 0; i--) {
-                stream.reset();
-                t = loaders.get(i).read(framework, stream);
+                t = loaders.get(i).read(framework, in);
                 if (t != null) {
                     return t; // we've loaded it
                 }