sunflow (0.07.2.svn396+dfsg-9) 30_sweethome3d

Summary

 src/SunflowGUI.java                                             |    6 
 src/org/sunflow/PluginRegistry.java                             |    2 
 src/org/sunflow/SunflowAPI.java                                 |    9 +
 src/org/sunflow/core/LightServer.java                           |   53 +++++-
 src/org/sunflow/core/Ray.java                                   |    7 
 src/org/sunflow/core/Shader.java                                |   14 +
 src/org/sunflow/core/ShadingState.java                          |   63 ++++++--
 src/org/sunflow/core/Texture.java                               |   77 +++++++++-
 src/org/sunflow/core/TextureCache.java                          |   12 +
 src/org/sunflow/core/accel/BoundingIntervalHierarchy.java       |   46 +++++
 src/org/sunflow/core/light/ImageBasedLight.java                 |   14 +
 src/org/sunflow/core/light/SphereLight.java                     |   10 +
 src/org/sunflow/core/light/SunSkyLight.java                     |   16 ++
 src/org/sunflow/core/light/TriangleMeshLight.java               |   10 +
 src/org/sunflow/core/modifiers/BumpMappingModifier.java         |    4 
 src/org/sunflow/core/modifiers/NormalMapModifier.java           |    4 
 src/org/sunflow/core/photonmap/GlobalPhotonMap.java             |    6 
 src/org/sunflow/core/primitive/CornellBox.java                  |   10 +
 src/org/sunflow/core/primitive/Hair.java                        |   10 +
 src/org/sunflow/core/renderer/BucketRenderer.java               |   37 +++-
 src/org/sunflow/core/renderer/MultipassRenderer.java            |   24 ++-
 src/org/sunflow/core/renderer/ProgressiveRenderer.java          |   26 ++-
 src/org/sunflow/core/renderer/SimpleRenderer.java               |   28 ++-
 src/org/sunflow/core/shader/AmbientOcclusionShader.java         |   10 +
 src/org/sunflow/core/shader/AnisotropicWardShader.java          |   10 +
 src/org/sunflow/core/shader/ConstantShader.java                 |   10 +
 src/org/sunflow/core/shader/DiffuseShader.java                  |   10 +
 src/org/sunflow/core/shader/GlassShader.java                    |   10 +
 src/org/sunflow/core/shader/IDShader.java                       |   10 +
 src/org/sunflow/core/shader/MirrorShader.java                   |   10 +
 src/org/sunflow/core/shader/NormalShader.java                   |   10 +
 src/org/sunflow/core/shader/PhongShader.java                    |   10 +
 src/org/sunflow/core/shader/PrimIDShader.java                   |   10 +
 src/org/sunflow/core/shader/QuickGrayShader.java                |   10 +
 src/org/sunflow/core/shader/ShinyDiffuseShader.java             |   16 ++
 src/org/sunflow/core/shader/SimpleShader.java                   |   10 +
 src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java |    4 
 src/org/sunflow/core/shader/TexturedDiffuseShader.java          |   34 ++++
 src/org/sunflow/core/shader/TexturedPhongShader.java            |   33 ++++
 src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java     |   58 +++++++
 src/org/sunflow/core/shader/TexturedWardShader.java             |   16 +-
 src/org/sunflow/core/shader/UVShader.java                       |   10 +
 src/org/sunflow/core/shader/UberShader.java                     |   76 ++++++---
 src/org/sunflow/core/shader/ViewCausticsShader.java             |   10 +
 src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java        |   10 +
 src/org/sunflow/core/shader/ViewIrradianceShader.java           |   10 +
 src/org/sunflow/core/shader/WireframeShader.java                |   10 +
 src/org/sunflow/image/Color.java                                |    6 
 src/org/sunflow/image/readers/BMPBitmapReader.java              |   26 ++-
 src/org/sunflow/image/readers/HDRBitmapReader.java              |   17 +-
 src/org/sunflow/image/readers/IGIBitmapReader.java              |   16 +-
 src/org/sunflow/image/readers/JPGBitmapReader.java              |   46 +++++
 src/org/sunflow/image/readers/PNGBitmapReader.java              |   51 +++++-
 src/org/sunflow/image/readers/TGABitmapReader.java              |   16 +-
 54 files changed, 938 insertions(+), 135 deletions(-)

    
download this patch

Patch contents

Description: Changes for sweethome3d. From sh3d upstream tarball, version 3.3.
Author: Emmanuel Puybaret, eTeks <info@eteks.com>

--- a/src/org/sunflow/core/accel/BoundingIntervalHierarchy.java
+++ b/src/org/sunflow/core/accel/BoundingIntervalHierarchy.java
@@ -177,7 +177,13 @@
         float clipL = Float.NaN, clipR = Float.NaN, prevClip = Float.NaN;
         float split = Float.NaN, prevSplit;
         boolean wasLeft = true;
-        while (true) {
+        // EP : Added a loop counter to avoid endless loop
+        float[] gridBoxCopy = gridBox.clone();
+        float[] nodeBoxCopy = nodeBox.clone();
+        int loopCount = 0;
+        // EP : End of modification
+        do {
+        // while (true) {
             prevAxis = axis;
             prevSplit = split;
             // perform quick consistency checks
@@ -256,7 +262,8 @@
             // ensure we are making progress in the subdivision
             if (right == rightOrig) {
                 // all left
-                if (clipL <= split) {
+                // EP : Added additional test to avoid endless loop
+                if (clipL < split || (clipL == split && !(prevAxis == axis && prevSplit == split))) {
                     // keep looping on left half
                     gridBox[2 * axis + 1] = split;
                     prevClip = clipL;
@@ -274,7 +281,8 @@
             } else if (left > right) {
                 // all right
                 right = rightOrig;
-                if (clipR >= split) {
+                // EP : Added additional test to avoid endless loop
+                if (clipR > split || (clipR == split && !(prevAxis == axis && prevSplit == split))) {
                     // keep looping on right half
                     gridBox[2 * axis + 0] = split;
                     prevClip = clipR;
@@ -322,7 +330,15 @@
                 }
                 break;
             }
+        // EP : Added test to avoid endless loop
+        } while (loopCount++ < 100);
+        if (loopCount > 100) {
+            System.arraycopy(gridBoxCopy, 0, gridBox, 0, gridBox.length);
+            System.arraycopy(nodeBoxCopy, 0, nodeBox, 0, nodeBox.length);
+            return;
         }
+        // EP : End of modification
+        
         // compute index of child nodes
         int nextIndex = tempTree.getSize();
         // allocate left node
@@ -482,8 +498,13 @@
                             intervalMax = (tf <= intervalMax) ? tf : intervalMax;
                             continue;
                         }
+                        // EP : Give up if stack is full
+                        if (stackPos == stack.length) {
+                            break pushloop;
+                        }
+                        // EP : End of modification
                         // ray passes through both nodes
-                        // push back node
+                        // push back node                        
                         stack[stackPos].node = back;
                         stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin;
                         stack[stackPos].far = intervalMax;
@@ -511,6 +532,11 @@
                             intervalMax = (tf <= intervalMax) ? tf : intervalMax;
                             continue;
                         }
+                        // EP : Give up if stack is full
+                        if (stackPos == stack.length) {
+                            break pushloop;
+                        }
+                        // EP : End of modification
                         // ray passes through both nodes
                         // push back node
                         stack[stackPos].node = back;
@@ -541,6 +567,11 @@
                             intervalMax = (tf <= intervalMax) ? tf : intervalMax;
                             continue;
                         }
+                        // EP : Give up if stack is full
+                        if (stackPos == stack.length) {
+                            break pushloop;
+                        }
+                        // EP : End of modification
                         // ray passes through both nodes
                         // push back node
                         stack[stackPos].node = back;
@@ -597,7 +628,12 @@
             } // traversal loop
             do {
                 // stack is empty?
-                if (stackPos == 0)
+                if (stackPos == 0 
+                    // EP : Check ray values aren't NaN
+                    || Float.isNaN(r.dx) 
+                    || Float.isNaN(r.dy) 
+                    || Float.isNaN(r.dz))
+                    // EP : End of modification
                     return;
                 // move back up the stack
                 stackPos--;
--- a/src/org/sunflow/core/light/ImageBasedLight.java
+++ b/src/org/sunflow/core/light/ImageBasedLight.java
@@ -11,7 +11,6 @@
 import org.sunflow.core.Shader;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Bitmap;
 import org.sunflow.image.Color;
 import org.sunflow.math.BoundingBox;
@@ -55,7 +54,8 @@
         numLowSamples = pl.getInt("lowsamples", numLowSamples);
         String filename = pl.getString("texture", null);
         if (filename != null)
-            texture = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            texture = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
 
         // no texture provided
         if (texture == null)
@@ -272,4 +272,14 @@
     public Instance createInstance() {
         return Instance.createTemporary(this, null, this);
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/light/SphereLight.java
+++ b/src/org/sunflow/core/light/SphereLight.java
@@ -150,4 +150,14 @@
     public Instance createInstance() {
         return Instance.createTemporary(new Sphere(), Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius)), this);
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/light/SunSkyLight.java
+++ b/src/org/sunflow/core/light/SunSkyLight.java
@@ -315,6 +315,12 @@
         return getSkyRGB(basis.untransform(state.getRay().getDirection())).constrainRGB();
     }
 
+    // EP : Reused sun sky color
+    public Color getSunColor() {
+        return getSkyRGB(basis.untransform(sunDirWorld)).constrainRGB();
+    }
+    // EP : End of modification
+    
     public void scatterPhoton(ShadingState state, Color power) {
         // let photon escape
     }
@@ -334,4 +340,14 @@
     public Instance createInstance() {
         return Instance.createTemporary(this, null, this);
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/light/TriangleMeshLight.java
+++ b/src/org/sunflow/core/light/TriangleMeshLight.java
@@ -269,4 +269,14 @@
             }
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/LightServer.java
+++ b/src/org/sunflow/core/LightServer.java
@@ -24,12 +24,15 @@
     private int maxDiffuseDepth;
     private int maxReflectionDepth;
     private int maxRefractionDepth;
+    // EP : Added transparency management  
+    private int maxTransparencyDepth;
 
     // indirect illumination
     private CausticPhotonMapInterface causticPhotonMap;
     private GIEngine giEngine;
     private int photonCounter;
 
+
     LightServer(Scene scene) {
         this.scene = scene;
         lights = new LightSource[0];
@@ -41,6 +44,8 @@
         maxDiffuseDepth = 1;
         maxReflectionDepth = 4;
         maxRefractionDepth = 4;
+        // EP : Added transparency management
+        maxTransparencyDepth = 4;
 
         causticPhotonMap = null;
         giEngine = null;
@@ -64,6 +69,8 @@
         maxDiffuseDepth = options.getInt("depths.diffuse", maxDiffuseDepth);
         maxReflectionDepth = options.getInt("depths.reflection", maxReflectionDepth);
         maxRefractionDepth = options.getInt("depths.refraction", maxRefractionDepth);
+        // EP : Added transparency management
+        maxTransparencyDepth = options.getInt("depths.transparency", maxTransparencyDepth);
         String giEngineType = options.getString("gi.engine", null);
         giEngine = PluginRegistry.giEnginePlugins.createObject(giEngineType);
         String caustics = options.getString("caustics", null);
@@ -143,7 +150,8 @@
                         synchronized (LightServer.this) {
                             UI.taskUpdate(photonCounter);
                             photonCounter++;
-                            if (UI.taskCanceled())
+                            // EP : Manage renderer stop with interruptions
+                            if (Thread.currentThread().isInterrupted())
                                 return;
                         }
 
@@ -176,18 +184,19 @@
             photonThreads[i].setPriority(scene.getThreadPriority());
             photonThreads[i].start();
         }
-        for (int i = 0; i < photonThreads.length; i++) {
-            try {
+        // EP : Moved InterruptedException out of loop to be able to stop all rendering threads
+        try {
+            for (int i = 0; i < photonThreads.length; i++) {
                 photonThreads[i].join();
-            } catch (InterruptedException e) {
-                UI.printError(Module.LIGHT, "Photon thread %d of %d was interrupted", i + 1, photonThreads.length);
-                return false;
             }
-        }
-        if (UI.taskCanceled()) {
-            UI.taskStop(); // shut down task cleanly
+        } catch (InterruptedException e) {
+            for (int i = 0; i < photonThreads.length; i++) {
+                photonThreads[i].interrupt();
+            }
+            UI.printError(Module.BCKT, "Photon thread was interrupted");
             return false;
         }
+        // EP : End of modification
         photonTimer.end();
         UI.taskStop();
         UI.printInfo(Module.LIGHT, "Tracing time for %s photons: %s", type, photonTimer.toString());
@@ -294,7 +303,7 @@
     Color shadeHit(ShadingState state) {
         state.getInstance().prepareShadingState(state);
         Shader shader = getShader(state);
-        return (shader != null) ? shader.getRadiance(state) : Color.BLACK;
+         return (shader != null) ? shader.getRadiance(state) : Color.BLACK;
     }
 
     Color traceGlossy(ShadingState previous, Ray r, int i) {
@@ -357,4 +366,28 @@
         if (causticPhotonMap != null)
             causticPhotonMap.getSamples(state);
     }
+    
+    // EP : Added transparency management  
+    Color traceShadow(Ray r, ShadingState previous) {
+        float maxDist = r.getMax();
+        scene.traceShadow(r, previous.getIntersectionState());
+        if (previous.getIntersectionState().hit()) {
+            Shader previousShader = previous.getIntersectionState().instance.getShader(0);
+            if (previousShader == null || previousShader.isOpaque() || previous.getShadowDepth() >= maxTransparencyDepth) {
+                return Color.WHITE; // fully opaque hit
+            }
+            ShadingState sstate = ShadingState.createShadowState(previous, r);
+            sstate.getInstance().prepareShadingState(sstate);
+            Shader shader = getShader(sstate);
+            if (shader == null || shader.isOpaque())
+                return Color.WHITE;
+            Color opac = shader.getOpacity(sstate);
+            if (opac.isWhite())
+                return opac;
+            else
+                return opac.copy().madd(Color.sub(Color.WHITE, opac), sstate.traceTransparentShadow(maxDist));
+        } else
+            return Color.BLACK;
+    }
+    // EP : End of modification  
 }
\ No newline at end of file
--- a/src/org/sunflow/core/modifiers/BumpMappingModifier.java
+++ b/src/org/sunflow/core/modifiers/BumpMappingModifier.java
@@ -5,7 +5,6 @@
 import org.sunflow.core.ParameterList;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.math.OrthoNormalBasis;
 
 public class BumpMappingModifier implements Modifier {
@@ -20,7 +19,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            bumpTexture = TextureCache.getTexture(api.resolveTextureFilename(filename), true);
+            // EP : Made texture cache local to a SunFlow API instance
+            bumpTexture = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), true);
         scale = pl.getFloat("scale", scale);
         return bumpTexture != null;
     }
--- a/src/org/sunflow/core/modifiers/NormalMapModifier.java
+++ b/src/org/sunflow/core/modifiers/NormalMapModifier.java
@@ -5,7 +5,6 @@
 import org.sunflow.core.ParameterList;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.math.OrthoNormalBasis;
 
 public class NormalMapModifier implements Modifier {
@@ -18,7 +17,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            normalMap = TextureCache.getTexture(api.resolveTextureFilename(filename), true);
+            // EP : Made texture cache local to a SunFlow API instance
+            normalMap = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), true);
         return normalMap != null;
     }
 
--- a/src/org/sunflow/core/photonmap/GlobalPhotonMap.java
+++ b/src/org/sunflow/core/photonmap/GlobalPhotonMap.java
@@ -260,7 +260,8 @@
         t.start();
         balance();
         t.end();
-        UI.taskStop();
+        // EP : Replaced task management with interruptions
+        // UI.taskStop();
         UI.printInfo(Module.LIGHT, "Global photon map:");
         UI.printInfo(Module.LIGHT, "  * Photons stored:   %d", storedPhotons);
         UI.printInfo(Module.LIGHT, "  * Photons/estimate: %d", numGather);
@@ -328,7 +329,8 @@
             curr.data = irr.toRGBE();
             temp[i] = curr;
         }
-        UI.taskStop();
+        // EP : Replaced task management with interruptions
+        // UI.taskStop();
 
         // resize photon map to only include irradiance photons
         numGather /= 4;
--- a/src/org/sunflow/core/primitive/CornellBox.java
+++ b/src/org/sunflow/core/primitive/CornellBox.java
@@ -443,4 +443,14 @@
     public Instance createInstance() {
         return Instance.createTemporary(this, null, this);
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/primitive/Hair.java
+++ b/src/org/sunflow/core/primitive/Hair.java
@@ -258,4 +258,14 @@
     public PrimitiveList getBakingPrimitives() {
         return null;
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/Ray.java
+++ b/src/org/sunflow/core/Ray.java
@@ -214,4 +214,11 @@
     public final void setMax(float t) {
         tMax = t;
     }
+
+    // EP : Added transparency management  
+    public void setMinMax(float min, float max) {
+        tMin = min;
+        tMax = max;
+    }
+    // EP : end of modification  
 }
\ No newline at end of file
--- a/src/org/sunflow/core/renderer/BucketRenderer.java
+++ b/src/org/sunflow/core/renderer/BucketRenderer.java
@@ -157,15 +157,22 @@
             renderThreads[i].setPriority(scene.getThreadPriority());
             renderThreads[i].start();
         }
-        for (int i = 0; i < renderThreads.length; i++) {
-            try {
-                renderThreads[i].join();
-            } catch (InterruptedException e) {
-                UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length);
-            } finally {
-                renderThreads[i].updateStats();
+        // EP : Moved InterruptedException out of loop to be able to stop all rendering threads
+        try {
+            for (int i = 0; i < renderThreads.length; i++) {
+                try {
+                    renderThreads[i].join();
+                } finally {
+                    renderThreads[i].updateStats();
+                }
+            }
+        } catch (InterruptedException e) {
+            for (int i = 0; i < renderThreads.length; i++) {
+                renderThreads[i].interrupt();
             }
+            UI.printError(Module.BCKT, "Bucket processing was interrupted");
         }
+        // EP : End of modification
         UI.taskStop();
         timer.end();
         UI.printInfo(Module.BCKT, "Render time: %s", timer.toString());
@@ -183,7 +190,8 @@
 
         @Override
         public void run() {
-            while (true) {
+            // EP : Check rendering isn't interrupted 
+            while (!isInterrupted()) {
                 int bx, by;
                 synchronized (BucketRenderer.this) {
                     if (bucketCounter >= bucketCoords.length)
@@ -194,8 +202,6 @@
                     bucketCounter += 2;
                 }
                 renderBucket(display, bx, by, threadID, istate);
-                if (UI.taskCanceled())
-                    return;
             }
         }
 
@@ -251,8 +257,13 @@
             }
         }
         for (int x = 0; x < sbw - 1; x += maxStepSize)
-            for (int y = 0; y < sbh - 1; y += maxStepSize)
+            for (int y = 0; y < sbh - 1; y += maxStepSize) {
+                // EP : Check rendering isn't interrupted
+                if (Thread.currentThread().isInterrupted()) {
+                    return;
+                }
                 refineSamples(samples, sbw, x, y, maxStepSize, thresh, istate);
+            }
         if (dumpBuckets) {
             UI.printInfo(Module.BCKT, "Dumping bucket [%d, %d] to file ...", bx, by);
             GenericBitmap bitmap = new GenericBitmap(sbw, sbh);
@@ -297,7 +308,9 @@
                             if (Math.abs(dy) > fhs)
                                 continue;
                             float f = filter.get(dx, dy);
-                            c.madd(f, samples[s].c);
+                            // EP : Test if color isn't null
+                            if (samples[s].c != null)
+                                c.madd(f, samples[s].c);
                             a += f * samples[s].alpha;
                             weight += f;
 
--- a/src/org/sunflow/core/renderer/MultipassRenderer.java
+++ b/src/org/sunflow/core/renderer/MultipassRenderer.java
@@ -86,15 +86,22 @@
             renderThreads[i].setPriority(scene.getThreadPriority());
             renderThreads[i].start();
         }
-        for (int i = 0; i < renderThreads.length; i++) {
-            try {
-                renderThreads[i].join();
-            } catch (InterruptedException e) {
-                UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length);
-            } finally {
-                renderThreads[i].updateStats();
+        // EP : Moved InterruptedException out of loop to be able to stop all rendering threads
+        try {
+            for (int i = 0; i < renderThreads.length; i++) {
+                try {
+                    renderThreads[i].join();
+                } finally {
+                    renderThreads[i].updateStats();
+                }
             }
+        } catch (InterruptedException e) {
+            for (int i = 0; i < renderThreads.length; i++) {
+                renderThreads[i].interrupt();
+            }
+            UI.printError(Module.BCKT, "Bucket processing was interrupted");
         }
+        // EP : End of modification
         UI.taskStop();
         timer.end();
         UI.printInfo(Module.BCKT, "Render time: %s", timer.toString());
@@ -114,7 +121,8 @@
 
         @Override
         public void run() {
-            while (true) {
+            // EP : Check rendering isn't interrupted or canceled
+            while (!isInterrupted()) {
                 int bx, by;
                 synchronized (MultipassRenderer.this) {
                     if (bucketCounter >= bucketCoords.length)
--- a/src/org/sunflow/core/renderer/ProgressiveRenderer.java
+++ b/src/org/sunflow/core/renderer/ProgressiveRenderer.java
@@ -58,15 +58,22 @@
             renderThreads[i] = new SmallBucketThread();
             renderThreads[i].start();
         }
-        for (int i = 0; i < renderThreads.length; i++) {
-            try {
-                renderThreads[i].join();
-            } catch (InterruptedException e) {
-                UI.printError(Module.IPR, "Thread %d of %d was interrupted", i + 1, renderThreads.length);
-            } finally {
-                renderThreads[i].updateStats();
+        // EP : Moved InterruptedException out of loop to be able to stop all rendering threads
+        try {
+            for (int i = 0; i < renderThreads.length; i++) {
+                try {
+                    renderThreads[i].join();
+                } finally {
+                    renderThreads[i].updateStats();
+                }
+            }
+        } catch (InterruptedException e) {
+            for (int i = 0; i < renderThreads.length; i++) {
+                renderThreads[i].interrupt();
             }
+            UI.printError(Module.IPR, "Thread was interrupted");
         }
+        // EP : End of modification
         UI.taskStop();
         t.end();
         UI.printInfo(Module.IPR, "Rendering time: %s", t.toString());
@@ -78,7 +85,8 @@
 
         @Override
         public void run() {
-            while (true) {
+            // EP : Check rendering isn't interrupted
+            while (!isInterrupted()) {
                 int n = progressiveRenderNext(istate);
                 synchronized (ProgressiveRenderer.this) {
                     if (counter >= counterMax)
@@ -86,8 +94,6 @@
                     counter += n;
                     UI.taskUpdate(counter);
                 }
-                if (UI.taskCanceled())
-                    return;
             }
         }
 
--- a/src/org/sunflow/core/renderer/SimpleRenderer.java
+++ b/src/org/sunflow/core/renderer/SimpleRenderer.java
@@ -41,15 +41,23 @@
             renderThreads[i] = new BucketThread();
             renderThreads[i].start();
         }
-        for (int i = 0; i < renderThreads.length; i++) {
-            try {
-                renderThreads[i].join();
-            } catch (InterruptedException e) {
-                UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length);
-            } finally {
-                renderThreads[i].updateStats();
+        // EP : Moved InterruptedException out of loop to be able to stop all rendering threads
+        try {
+            for (int i = 0; i < renderThreads.length; i++) {
+                try {
+                    renderThreads[i].join();
+                } finally {
+                    renderThreads[i].updateStats();
+                }
+            }
+        } catch (InterruptedException e) {
+            for (int i = 0; i < renderThreads.length; i++) {
+                renderThreads[i].interrupt();
             }
+            UI.printError(Module.BCKT, "Bucket processing was interrupted");
         }
+        UI.taskStop();
+        // EP : End of modification
         timer.end();
         UI.printInfo(Module.BCKT, "Render time: %s", timer.toString());
         display.imageEnd();
@@ -60,7 +68,8 @@
 
         @Override
         public void run() {
-            while (true) {
+            // EP : Check rendering isn't interrupted 
+            while (!isInterrupted()) {
                 int bx, by;
                 synchronized (SimpleRenderer.this) {
                     if (bucketCounter >= numBuckets)
@@ -90,6 +99,9 @@
 
         for (int y = 0, i = 0; y < bh; y++) {
             for (int x = 0; x < bw; x++, i++) {
+                // EP : Check rendering isn't interrupted
+                if (Thread.currentThread().isInterrupted())
+                    return;
                 ShadingState state = scene.getRadiance(istate, x0 + x, imageHeight - 1 - (y0 + y), 0.0, 0.0, 0.0, 0, 0, null);
                 bucketRGB[i] = (state != null) ? state.getResult() : Color.BLACK;
                 bucketAlpha[i] = (state != null) ? 1 : 0;
--- a/src/org/sunflow/core/shader/AmbientOcclusionShader.java
+++ b/src/org/sunflow/core/shader/AmbientOcclusionShader.java
@@ -45,4 +45,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+    
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/AnisotropicWardShader.java
+++ b/src/org/sunflow/core/shader/AnisotropicWardShader.java
@@ -208,4 +208,14 @@
             state.traceReflectionPhoton(r, power);
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/ConstantShader.java
+++ b/src/org/sunflow/core/shader/ConstantShader.java
@@ -24,4 +24,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/DiffuseShader.java
+++ b/src/org/sunflow/core/shader/DiffuseShader.java
@@ -58,4 +58,14 @@
             state.traceDiffusePhoton(new Ray(state.getPoint(), w), power);
         }
     }
+    
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/GlassShader.java
+++ b/src/org/sunflow/core/shader/GlassShader.java
@@ -136,4 +136,14 @@
             }
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return absorptionColor.isWhite();
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return absorptionColor;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/IDShader.java
+++ b/src/org/sunflow/core/shader/IDShader.java
@@ -20,4 +20,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/MirrorShader.java
+++ b/src/org/sunflow/core/shader/MirrorShader.java
@@ -59,4 +59,14 @@
         dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z;
         state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power);
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/NormalShader.java
+++ b/src/org/sunflow/core/shader/NormalShader.java
@@ -24,4 +24,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/PhongShader.java
+++ b/src/org/sunflow/core/shader/PhongShader.java
@@ -82,4 +82,14 @@
             state.traceReflectionPhoton(new Ray(state.getPoint(), w), power);
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/PrimIDShader.java
+++ b/src/org/sunflow/core/shader/PrimIDShader.java
@@ -23,4 +23,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/QuickGrayShader.java
+++ b/src/org/sunflow/core/shader/QuickGrayShader.java
@@ -56,4 +56,14 @@
             state.traceDiffusePhoton(new Ray(state.getPoint(), w), power);
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/ShinyDiffuseShader.java
+++ b/src/org/sunflow/core/shader/ShinyDiffuseShader.java
@@ -24,6 +24,12 @@
         return true;
     }
 
+    // EP : Added getter to read shininess from subclasses
+    protected float getShininess() {
+        return this.refl;
+    }
+    // EP : End of modification
+    
     public Color getDiffuse(ShadingState state) {
         return diff;
     }
@@ -90,4 +96,14 @@
             state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power);
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/SimpleShader.java
+++ b/src/org/sunflow/core/shader/SimpleShader.java
@@ -17,4 +17,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java
+++ b/src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java
@@ -4,7 +4,6 @@
 import org.sunflow.core.ParameterList;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Color;
 
 public class TexturedAmbientOcclusionShader extends AmbientOcclusionShader {
@@ -18,7 +17,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            tex = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         return tex != null && super.update(pl, api);
     }
 
--- a/src/org/sunflow/core/shader/TexturedDiffuseShader.java
+++ b/src/org/sunflow/core/shader/TexturedDiffuseShader.java
@@ -2,10 +2,11 @@
 
 import org.sunflow.SunflowAPI;
 import org.sunflow.core.ParameterList;
+import org.sunflow.core.Ray;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Color;
+import org.sunflow.math.Vector3;
 
 public class TexturedDiffuseShader extends DiffuseShader {
     private Texture tex;
@@ -18,7 +19,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            tex = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         return tex != null && super.update(pl, api);
     }
 
@@ -26,4 +28,32 @@
     public Color getDiffuse(ShadingState state) {
         return tex.getPixel(state.getUV().x, state.getUV().y);
     }
+
+    // EP : Added transparency management  
+    @Override
+    public Color getRadiance(ShadingState state) {
+        Color opacity;
+        if (isOpaque() || (opacity = getOpacity(state)).isWhite()) {
+            // Pixel is fully opaque
+            return super.getRadiance(state);
+        } else {
+            state.faceforward();
+            state.initLightSamples();
+            state.initCausticSamples();
+            Vector3 refrDir = state.getRay().getDirection();
+            Color refraction = state.traceRefraction(new Ray(state.getPoint(), refrDir), 0);
+            return Color.sub(Color.WHITE, opacity).mul(refraction);
+        }
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return !(tex.isTransparent());
+    }
+    
+    @Override
+    public Color getOpacity(ShadingState state) {
+        return tex.getOpacity(state.getUV().x, state.getUV().y);
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/TexturedPhongShader.java
+++ b/src/org/sunflow/core/shader/TexturedPhongShader.java
@@ -2,10 +2,11 @@
 
 import org.sunflow.SunflowAPI;
 import org.sunflow.core.ParameterList;
+import org.sunflow.core.Ray;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Color;
+import org.sunflow.math.Vector3;
 
 public class TexturedPhongShader extends PhongShader {
     private Texture tex;
@@ -18,7 +19,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            tex = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         return tex != null && super.update(pl, api);
     }
 
@@ -26,4 +28,31 @@
     public Color getDiffuse(ShadingState state) {
         return tex.getPixel(state.getUV().x, state.getUV().y);
     }
+
+    // EP : Added transparency management  
+    @Override
+    public Color getRadiance(ShadingState state) {
+        Color opacity;
+        if (isOpaque() || (opacity = getOpacity(state)).isWhite()) {
+            return super.getRadiance(state);
+        } else {
+            state.faceforward();
+            state.initLightSamples();
+            state.initCausticSamples();
+            Vector3 refrDir = state.getRay().getDirection();
+            Color refraction = state.traceRefraction(new Ray(state.getPoint(), refrDir), 0);
+            return Color.sub(Color.WHITE, opacity).mul(refraction);
+        }
+    }
+    
+    @Override
+    public boolean isOpaque() {
+        return !(tex.isTransparent());
+    }
+    
+    @Override
+    public Color getOpacity(ShadingState state) {
+        return tex.getOpacity(state.getUV().x, state.getUV().y);
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java
+++ b/src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java
@@ -2,10 +2,11 @@
 
 import org.sunflow.SunflowAPI;
 import org.sunflow.core.ParameterList;
+import org.sunflow.core.Ray;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Color;
+import org.sunflow.math.Vector3;
 
 public class TexturedShinyDiffuseShader extends ShinyDiffuseShader {
     private Texture tex;
@@ -18,7 +19,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            tex = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         return tex != null && super.update(pl, api);
     }
 
@@ -26,4 +28,56 @@
     public Color getDiffuse(ShadingState state) {
         return tex.getPixel(state.getUV().x, state.getUV().y);
     }
+
+    // EP : Added transparency management  
+    @Override
+    public Color getRadiance(ShadingState state) {
+        Color opacity;
+        if (isOpaque() || (opacity = getOpacity(state)).isWhite()) {
+            // Pixel is fully opaque
+            return super.getRadiance(state);
+        } else {
+            state.faceforward();
+            // direct lighting
+            state.initLightSamples();
+            state.initCausticSamples();
+            Color d = Color.sub(Color.WHITE, opacity);
+            Vector3 refrDir = state.getRay().getDirection();
+            Color refraction = state.traceRefraction(new Ray(state.getPoint(), refrDir), 0);
+            d.mul(refraction);
+            if (!state.includeSpecular()
+                || opacity.isBlack()) { // No reflection when fully transparent
+                return d;
+            }
+            float cos = state.getCosND();
+            float dn = 2 * cos;
+            Vector3 refDir = new Vector3();
+            refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x;
+            refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y;
+            refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z;
+            Ray refRay = new Ray(state.getPoint(), refDir);
+            // compute Fresnel term
+            cos = 1 - cos;
+            float cos2 = cos * cos;
+            float cos5 = cos2 * cos2 * cos;
+
+            Color ret = Color.white();
+            Color r = Color.sub(Color.WHITE, opacity).mul(getShininess());
+            ret.sub(r);
+            ret.mul(cos5);
+            ret.add(r);
+            return d.add(ret.mul(state.traceReflection(refRay, 0)));
+        }
+    }
+    
+    @Override
+    public boolean isOpaque() {
+        return !(tex.isTransparent());
+    }
+    
+    @Override
+    public Color getOpacity(ShadingState state) {
+        return tex.getOpacity(state.getUV().x, state.getUV().y);
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/TexturedWardShader.java
+++ b/src/org/sunflow/core/shader/TexturedWardShader.java
@@ -4,7 +4,6 @@
 import org.sunflow.core.ParameterList;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Color;
 
 public class TexturedWardShader extends AnisotropicWardShader {
@@ -18,7 +17,8 @@
     public boolean update(ParameterList pl, SunflowAPI api) {
         String filename = pl.getString("texture", null);
         if (filename != null)
-            tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            tex = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         return tex != null && super.update(pl, api);
     }
 
@@ -26,4 +26,16 @@
     public Color getDiffuse(ShadingState state) {
         return tex.getPixel(state.getUV().x, state.getUV().y);
     }
+
+    // EP : Added transparency management  
+    @Override
+    public boolean isOpaque() {
+        return !(tex.isTransparent());
+    }
+    
+    @Override
+    public Color getOpacity(ShadingState state) {
+        return tex.getOpacity(state.getUV().x, state.getUV().y);
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/UberShader.java
+++ b/src/org/sunflow/core/shader/UberShader.java
@@ -6,7 +6,6 @@
 import org.sunflow.core.Shader;
 import org.sunflow.core.ShadingState;
 import org.sunflow.core.Texture;
-import org.sunflow.core.TextureCache;
 import org.sunflow.image.Color;
 import org.sunflow.math.MathUtils;
 import org.sunflow.math.OrthoNormalBasis;
@@ -36,10 +35,12 @@
         String filename;
         filename = pl.getString("diffuse.texture", null);
         if (filename != null)
-            diffmap = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            diffmap = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         filename = pl.getString("specular.texture", null);
         if (filename != null)
-            specmap = TextureCache.getTexture(api.resolveTextureFilename(filename), false);
+            // EP : Made texture cache local to a SunFlow API instance
+            specmap = api.getTextureCache().getTexture(api.resolveTextureFilename(filename), false);
         diffBlend = MathUtils.clamp(pl.getFloat("diffuse.blend", diffBlend), 0, 1);
         specBlend = MathUtils.clamp(pl.getFloat("specular.blend", diffBlend), 0, 1);
         glossyness = MathUtils.clamp(pl.getFloat("glossyness", glossyness), 0, 1);
@@ -61,30 +62,41 @@
         // direct lighting
         state.initLightSamples();
         state.initCausticSamples();
-        Color d = getDiffuse(state);
-        Color lr = state.diffuse(d);
-        if (!state.includeSpecular())
-            return lr;
-        if (glossyness == 0) {
-            float cos = state.getCosND();
-            float dn = 2 * cos;
-            Vector3 refDir = new Vector3();
-            refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x;
-            refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y;
-            refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z;
-            Ray refRay = new Ray(state.getPoint(), refDir);
-            // compute Fresnel term
-            cos = 1 - cos;
-            float cos2 = cos * cos;
-            float cos5 = cos2 * cos2 * cos;
-            Color spec = getSpecular(state);
-            Color ret = Color.white();
-            ret.sub(spec);
-            ret.mul(cos5);
-            ret.add(spec);
-            return lr.add(ret.mul(state.traceReflection(refRay, 0)));
-        } else
-            return lr.add(state.specularPhong(getSpecular(state), 2 / glossyness, numSamples));
+        // EP : Added transparency management  
+        Color opacity;
+        if (!isOpaque() && !(opacity = getOpacity(state)).isWhite()) {
+            Vector3 refrDir = state.getRay().getDirection();
+            Color refraction = state.traceRefraction(new Ray(state.getPoint(), refrDir), 0);
+            return Color.sub(Color.WHITE, opacity).mul(refraction);
+        } else {
+        // EP : End of modification
+            Color d = getDiffuse(state);
+            Color lr = state.diffuse(d);
+            if (!state.includeSpecular())
+                return lr;
+            if (glossyness == 0) {
+                float cos = state.getCosND();
+                float dn = 2 * cos;
+                Vector3 refDir = new Vector3();
+                refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x;
+                refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y;
+                refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z;
+                Ray refRay = new Ray(state.getPoint(), refDir);
+                // compute Fresnel term
+                cos = 1 - cos;
+                float cos2 = cos * cos;
+                float cos5 = cos2 * cos2 * cos;
+                Color spec = getSpecular(state);
+                Color ret = Color.white();
+                ret.sub(spec);
+                ret.mul(cos5);
+                ret.add(spec);
+                return lr.add(ret.mul(state.traceReflection(refRay, 0)));
+            } else
+                return lr.add(state.specularPhong(getSpecular(state), 2 / glossyness, numSamples));
+        // EP : Added transparency management  
+        }
+        // EP : End of modification
     }
 
     public void scatterPhoton(ShadingState state, Color power) {
@@ -138,4 +150,14 @@
             }
         }
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return diffmap == null || !(diffmap.isTransparent());
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return diffmap != null ? diffmap.getOpacity(state.getUV().x, state.getUV().y) : Color.WHITE;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/UVShader.java
+++ b/src/org/sunflow/core/shader/UVShader.java
@@ -19,4 +19,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/ViewCausticsShader.java
+++ b/src/org/sunflow/core/shader/ViewCausticsShader.java
@@ -25,4 +25,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java
+++ b/src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java
@@ -18,4 +18,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/ViewIrradianceShader.java
+++ b/src/org/sunflow/core/shader/ViewIrradianceShader.java
@@ -18,4 +18,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/shader/WireframeShader.java
+++ b/src/org/sunflow/core/shader/WireframeShader.java
@@ -73,4 +73,14 @@
 
     public void scatterPhoton(ShadingState state, Color power) {
     }
+
+    // EP : Added transparency management  
+    public boolean isOpaque() {
+        return true;
+    }
+    
+    public Color getOpacity(ShadingState state) {
+        return null;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/Shader.java
+++ b/src/org/sunflow/core/Shader.java
@@ -26,4 +26,18 @@
      * @param power power of the incoming photon.
      */
     public void scatterPhoton(ShadingState state, Color power);
+
+    // EP : Added transparency management  
+    /**
+     * Returns <code>true</code> if this shader is fully opaque. 
+     * This gives a quick way to find out if a shader needs further processing 
+     * when hit by a shadow ray. 
+     */
+    public boolean isOpaque(); 
+    
+    /**
+     * Returns how much light is blocked by this shader.  
+     */
+    public Color getOpacity(ShadingState state);    
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/org/sunflow/core/ShadingState.java
+++ b/src/org/sunflow/core/ShadingState.java
@@ -46,9 +46,12 @@
     private boolean includeSpecular;
     private LightSample lightSample;
     private PhotonStore map;
+    // EP : Added transparency management  
+    private int shadowDepth;
 
     static ShadingState createPhotonState(Ray r, IntersectionState istate, int i, PhotonStore map, LightServer server) {
-        ShadingState s = new ShadingState(null, istate, r, i, 4);
+        // EP : Added ignoreHalton parameter 
+        ShadingState s = new ShadingState(null, istate, r, i, 4, false);
         s.server = server;
         s.map = map;
         return s;
@@ -56,7 +59,8 @@
     }
 
     static ShadingState createState(IntersectionState istate, float rx, float ry, float time, Ray r, int i, int d, LightServer server) {
-        ShadingState s = new ShadingState(null, istate, r, i, d);
+        // EP : Added ignoreHalton parameter 
+        ShadingState s = new ShadingState(null, istate, r, i, d, false);
         s.server = server;
         s.rx = rx;
         s.ry = ry;
@@ -65,40 +69,48 @@
     }
 
     static ShadingState createDiffuseBounceState(ShadingState previous, Ray r, int i) {
-        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2);
+        // EP : Added ignoreHalton parameter 
+        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2, false);
         s.diffuseDepth++;
         return s;
     }
 
     static ShadingState createGlossyBounceState(ShadingState previous, Ray r, int i) {
-        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2);
+        // EP : Added ignoreHalton parameter 
+        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2, false);
         s.includeLights = false;
-        s.includeSpecular = false;
-        s.reflectionDepth++;
+        // EP : Set includeSpecular to true to get the reflects
+        s.includeSpecular = true;
+        // EP : Very dirty hack to let mirror shader manage more bounces than uber shader 
+        s.reflectionDepth += 4;
         return s;
     }
 
     static ShadingState createReflectionBounceState(ShadingState previous, Ray r, int i) {
-        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2);
+        // EP : Added ignoreHalton parameter 
+        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2, false);
         s.reflectionDepth++;
         return s;
     }
 
     static ShadingState createRefractionBounceState(ShadingState previous, Ray r, int i) {
-        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2);
+        // EP : Added ignoreHalton parameter 
+        ShadingState s = new ShadingState(previous, previous.istate, r, i, 2, false);
         s.refractionDepth++;
         return s;
     }
 
     static ShadingState createFinalGatherState(ShadingState state, Ray r, int i) {
-        ShadingState finalGatherState = new ShadingState(state, state.istate, r, i, 2);
+        // EP : Added ignoreHalton parameter 
+        ShadingState finalGatherState = new ShadingState(state, state.istate, r, i, 2, false);
         finalGatherState.diffuseDepth++;
         finalGatherState.includeLights = false;
         finalGatherState.includeSpecular = false;
         return finalGatherState;
     }
 
-    private ShadingState(ShadingState previous, IntersectionState istate, Ray r, int i, int d) {
+    // EP : Added ignoreHalton parameter 
+    private ShadingState(ShadingState previous, IntersectionState istate, Ray r, int i, int d, boolean ignoreHalton) {
         this.r = r;
         this.istate = istate;
         this.i = i;
@@ -121,6 +133,8 @@
             diffuseDepth = previous.diffuseDepth;
             reflectionDepth = previous.reflectionDepth;
             refractionDepth = previous.refractionDepth;
+            // EP : copy shadow depth
+            shadowDepth = previous.shadowDepth;
             server = previous.server;
             map = previous.map;
             rx = previous.rx;
@@ -131,8 +145,12 @@
         behind = false;
         cosND = Float.NaN;
         includeLights = includeSpecular = true;
-        qmcD0I = QMC.halton(this.d, this.i);
-        qmcD1I = QMC.halton(this.d + 1, this.i);
+        // EP : Ignore Halton values for transparency computing
+        if (!ignoreHalton) {
+            qmcD0I = QMC.halton(this.d, this.i);
+            qmcD1I = QMC.halton(this.d + 1, this.i);
+        }
+        // EP : End of modification 
         result = null;
         bias = 0.001f;
     }
@@ -691,7 +709,8 @@
      * @return opacity along the shadow ray
      */
     public final Color traceShadow(Ray r) {
-        return server.getScene().traceShadow(r, istate);
+        // EP : Added  transparency management
+        return server.traceShadow(r, this);
     }
 
     /**
@@ -926,4 +945,22 @@
             throw new UnsupportedOperationException();
         }
     }
+
+    // EP : Added transparency management  
+    static ShadingState createShadowState(ShadingState previous, Ray r) {
+        ShadingState s = new ShadingState(previous, previous.istate, r, previous.i, previous.d, true);
+        s.shadowDepth++;
+        return s;
+    }
+    
+    public final int getShadowDepth() {
+        return shadowDepth;
+    }
+
+    public Color traceTransparentShadow(float oldMaxT) {
+        Ray tr = new Ray(r.ox, r.oy, r.oz, r.dx, r.dy, r.dz);
+        tr.setMinMax(r.getMax(), oldMaxT);
+        return traceShadow(tr);
+    }
+    // EP : end of modification  
 }
\ No newline at end of file
--- a/src/org/sunflow/core/TextureCache.java
+++ b/src/org/sunflow/core/TextureCache.java
@@ -10,9 +10,11 @@
  * texture might be used more than once in your scene.
  */
 public final class TextureCache {
-    private static HashMap<String, Texture> textures = new HashMap<String, Texture>();
+    // EP : Removed static to enable GC to free Texture memory
+    private HashMap<String, Texture> textures = new HashMap<String, Texture>();
 
-    private TextureCache() {
+    // EP : Made texture cache local to SunFlow API
+    public TextureCache() {
     }
 
     /**
@@ -25,7 +27,8 @@
      * @return texture object
      * @see Texture
      */
-    public synchronized static Texture getTexture(String filename, boolean isLinear) {
+    // EP : Removed static to enable GC to free Texture memory
+    public synchronized Texture getTexture(String filename, boolean isLinear) {
         if (textures.containsKey(filename)) {
             UI.printInfo(Module.TEX, "Using cached copy for file \"%s\" ...", filename);
             return textures.get(filename);
@@ -40,7 +43,8 @@
      * Flush all textures from the cache, this will cause them to be reloaded
      * anew the next time they are accessed.
      */
-    public synchronized static void flush() {
+    // EP : Removed static to enable GC to free Texture memory
+    public synchronized void flush() {
         UI.printInfo(Module.TEX, "Flushing texture cache");
         textures.clear();
     }
--- a/src/org/sunflow/core/Texture.java
+++ b/src/org/sunflow/core/Texture.java
@@ -1,6 +1,8 @@
 package org.sunflow.core;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
 
 import org.sunflow.PluginRegistry;
 import org.sunflow.image.Bitmap;
@@ -23,6 +25,8 @@
     private boolean isLinear;
     private Bitmap bitmap;
     private int loaded;
+    // EP : Added bitmap transparency support
+    private boolean isTransparent;
 
     /**
      * Creates a new texture from the specfied file.
@@ -43,11 +47,38 @@
         try {
             UI.printInfo(Module.TEX, "Reading texture bitmap from: \"%s\" ...", filename);
             BitmapReader reader = PluginRegistry.bitmapReaderPlugins.createObject(extension);
+            // EP : Tolerate no extension in URLs
+            if (reader == null) {
+                try {
+                    // Choose a reader depending on the magic number of the file
+                    URL url = new URL(filename);
+                    InputStream in = url.openStream();
+                    int firstByte = in.read();
+                    int secondByte = in.read();
+                    in.close();                    
+                    reader = firstByte == 0xFF && secondByte == 0xD8
+                        ? PluginRegistry.bitmapReaderPlugins.createObject("jpg")
+                        : PluginRegistry.bitmapReaderPlugins.createObject("png");
+                } catch (IOException ex) {  
+                    // Don't try to search an other reader
+                }
+            }
+            // EP : End of modification
             if (reader != null) {
                 bitmap = reader.load(filename, isLinear);
                 if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0)
                     bitmap = null;
             }
+            // EP : Check transparency
+            for (int x = 0; x < bitmap.getWidth(); x++) {
+                for (int y = 0; y < bitmap.getHeight(); y++) {
+                    if (bitmap.readAlpha(x, y) < 1) {
+                        this.isTransparent = true;
+                        break;
+                    }
+                }
+            }
+            // EP : End of modification
             if (bitmap == null) {
                 UI.printError(Module.TEX, "Bitmap reading failed");
                 bitmap = new BitmapBlack();
@@ -105,7 +136,51 @@
         c.madd(k11, c11);
         return c;
     }
-
+    
+    // EP : Added bitmap transparency support
+    public Color getOpacity(float x, float y) {
+        Bitmap bitmap = getBitmap();
+        x = MathUtils.frac(x);
+        y = MathUtils.frac(y);
+        float dx = x * (bitmap.getWidth() - 1);
+        float dy = y * (bitmap.getHeight() - 1);
+        int ix0 = (int) dx;
+        int iy0 = (int) dy;
+        int ix1 = (ix0 + 1) % bitmap.getWidth();
+        int iy1 = (iy0 + 1) % bitmap.getHeight();
+        float u = dx - ix0;
+        float v = dy - iy0;
+        u = u * u * (3.0f - (2.0f * u));
+        v = v * v * (3.0f - (2.0f * v));
+        float k00 = (1.0f - u) * (1.0f - v);
+        float a00 = bitmap.readAlpha(ix0, iy0);
+        float k01 = (1.0f - u) * v;
+        float a01 = bitmap.readAlpha(ix0, iy1);
+        float k10 = u * (1.0f - v);
+        float a10 = bitmap.readAlpha(ix1, iy0);
+        float k11 = u * v;
+        float a11 = bitmap.readAlpha(ix1, iy1);
+        float transparency = k00 * a00 +  k01 * a01 + k10 * a10 + k11 * a11;
+        if (transparency <= 0.9999) {
+            Color c00 = bitmap.readColor(ix0, iy0);
+            Color c01 = bitmap.readColor(ix0, iy1);
+            Color c10 = bitmap.readColor(ix1, iy0);
+            Color c11 = bitmap.readColor(ix1, iy1);
+            Color c = Color.mul(k00, c00);
+            c.madd(k01, c01);
+            c.madd(k10, c10);
+            c.madd(k11, c11);
+            return c.opposite().mul(transparency);
+        } else {
+            return Color.WHITE; 
+        }
+    }
+    
+    public boolean isTransparent() {
+        return this.isTransparent;
+    }
+    // EP : End of modification
+    
     public Vector3 getNormal(float x, float y, OrthoNormalBasis basis) {
         float[] rgb = getPixel(x, y).getRGB();
         return basis.transform(new Vector3(2 * rgb[0] - 1, 2 * rgb[1] - 1, 2 * rgb[2] - 1)).normalize();
--- a/src/org/sunflow/image/Color.java
+++ b/src/org/sunflow/image/Color.java
@@ -116,6 +116,12 @@
         return r <= 0 && g <= 0 && b <= 0;
     }
 
+    // EP : Added to manage white colors
+    public boolean isWhite() {
+        return r >= 1 && g >= 1 && b >= 1;
+    }
+    // EP : End of modification
+
     public final float getLuminance() {
         return (0.2989f * r) + (0.5866f * g) + (0.1145f * b);
     }
--- a/src/org/sunflow/image/readers/BMPBitmapReader.java
+++ b/src/org/sunflow/image/readers/BMPBitmapReader.java
@@ -1,8 +1,11 @@
 package org.sunflow.image.readers;
 
 import java.awt.image.BufferedImage;
-import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import javax.imageio.ImageIO;
 
@@ -13,8 +16,25 @@
 
 public class BMPBitmapReader implements BitmapReader {
     public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException {
-        // regular image, load using Java api - ignore alpha channel
-        BufferedImage bi = ImageIO.read(new File(filename));
+        // EP : Try to read filename as an URL or as a file
+        InputStream f;
+        try {
+            // Let's try first to read filename as an URL
+            f = new URL(filename).openStream();
+        } catch (MalformedURLException ex) {
+            // Let's try to read filename as a file
+            f = new FileInputStream(filename);
+        }
+
+        BufferedImage bi;
+        try {
+            // regular image, load using Java api - ignore alpha channel
+            bi = ImageIO.read(f);
+        } finally {
+            f.close();
+        }
+        // EP : End of modification
+
         int width = bi.getWidth();
         int height = bi.getHeight();
         byte[] pixels = new byte[3 * width * height];
--- a/src/org/sunflow/image/readers/HDRBitmapReader.java
+++ b/src/org/sunflow/image/readers/HDRBitmapReader.java
@@ -4,6 +4,8 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import org.sunflow.image.Bitmap;
 import org.sunflow.image.BitmapReader;
@@ -11,8 +13,19 @@
 
 public class HDRBitmapReader implements BitmapReader {
     public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException {
-        // load radiance rgbe file
-        InputStream f = new BufferedInputStream(new FileInputStream(filename));
+        // EP : Try to read filename as an URL or as a file
+        InputStream f;
+        try {
+          // Let's try first to read filename as an URL
+          f = new URL(filename).openStream();
+        } catch (MalformedURLException ex) {
+          // Let's try to read filename as a file
+          f = new FileInputStream(filename);
+        }
+
+        f = new BufferedInputStream(f);
+        // End of modification
+
         // parse header
         boolean parseWidth = false, parseHeight = false;
         int width = 0, height = 0;
--- a/src/org/sunflow/image/readers/IGIBitmapReader.java
+++ b/src/org/sunflow/image/readers/IGIBitmapReader.java
@@ -4,6 +4,8 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import org.sunflow.image.Bitmap;
 import org.sunflow.image.BitmapReader;
@@ -15,7 +17,19 @@
  */
 public class IGIBitmapReader implements BitmapReader {
     public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException {
-        InputStream stream = new BufferedInputStream(new FileInputStream(filename));
+        // EP : Try to read filename as an URL or as a file
+        InputStream stream;
+        try {
+          // Let's try first to read filename as an URL
+          stream = new URL(filename).openStream();
+        } catch (MalformedURLException ex) {
+          // Let's try to read filename as a file
+          stream = new FileInputStream(filename);
+        }
+
+        stream = new BufferedInputStream(stream);
+        // End of modification
+
         // read header
         int magic = read32i(stream);
         int version = read32i(stream);
--- a/src/org/sunflow/image/readers/JPGBitmapReader.java
+++ b/src/org/sunflow/image/readers/JPGBitmapReader.java
@@ -1,8 +1,13 @@
 package org.sunflow.image.readers;
 
+import java.awt.Graphics2D;
+import java.awt.Transparency;
 import java.awt.image.BufferedImage;
-import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import javax.imageio.ImageIO;
 
@@ -13,14 +18,47 @@
 
 public class JPGBitmapReader implements BitmapReader {
     public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException {
-        // regular image, load using Java api - ignore alpha channel
-        BufferedImage bi = ImageIO.read(new File(filename));
+        // EP : Try to read filename as an URL or as a file
+        InputStream f;
+        try {
+            // Let's try first to read filename as an URL
+            f = new URL(filename).openStream();
+        } catch (MalformedURLException ex) {
+            // Let's try to read filename as a file
+            f = new FileInputStream(filename);
+        }
+
+        BufferedImage bi;
+        try {
+            // regular image, load using Java api - ignore alpha channel
+            bi = ImageIO.read(f);
+        } finally {
+            f.close();
+        }
+        
+        if (bi.getType() != BufferedImage.TYPE_INT_RGB
+            && bi.getType() != BufferedImage.TYPE_INT_ARGB) {
+          // Transform as TYPE_INT_ARGB or TYPE_INT_RGB (much faster than calling image.getRGB())
+          BufferedImage tmp = new BufferedImage(bi.getWidth(), bi.getHeight(), 
+                  bi.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB);
+          Graphics2D g = (Graphics2D)tmp.getGraphics();
+          g.drawImage(bi, null, 0, 0);
+          g.dispose();
+          bi = tmp;
+        }
+        // Retrieve image bits
+        int [] imageBits = (int [])bi.getRaster().getDataElements(0, 0, bi.getWidth(), bi.getHeight(), null);
+        // EP : End of modification
+
         int width = bi.getWidth();
         int height = bi.getHeight();
         byte[] pixels = new byte[3 * width * height];
         for (int y = 0, index = 0; y < height; y++) {
             for (int x = 0; x < width; x++, index += 3) {
-                int argb = bi.getRGB(x, height - 1 - y);
+                // EP : Retrieved image data with raster data 
+                // int argb = bi.getRGB(x, height - 1 - y);
+                int argb = imageBits [x + (height - 1 - y) * width];                
+                // EP : End of modification
                 pixels[index + 0] = (byte) (argb >> 16);
                 pixels[index + 1] = (byte) (argb >> 8);
                 pixels[index + 2] = (byte) argb;
--- a/src/org/sunflow/image/readers/PNGBitmapReader.java
+++ b/src/org/sunflow/image/readers/PNGBitmapReader.java
@@ -1,8 +1,13 @@
 package org.sunflow.image.readers;
 
+import java.awt.Graphics2D;
+import java.awt.Transparency;
 import java.awt.image.BufferedImage;
-import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import javax.imageio.ImageIO;
 
@@ -13,18 +18,54 @@
 
 public class PNGBitmapReader implements BitmapReader {
     public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException {
-        // regular image, load using Java api
-        BufferedImage bi = ImageIO.read(new File(filename));
+        // EP : Try to read filename as an URL or as a file
+        InputStream f;
+        try {
+            // Let's try first to read filename as an URL
+            f = new URL(filename).openStream();
+        } catch (MalformedURLException ex) {
+            // Let's try to read filename as a file
+            f = new FileInputStream(filename);
+        }
+
+        BufferedImage bi;
+        try {
+            // regular image, load using Java api 
+            bi = ImageIO.read(f);
+        } finally {
+            f.close();
+        }
+        
+        boolean opaque = bi.getTransparency() == Transparency.OPAQUE;
+        if (bi.getType() != BufferedImage.TYPE_INT_RGB
+            && bi.getType() != BufferedImage.TYPE_INT_ARGB) {
+            // Transform as TYPE_INT_ARGB or TYPE_INT_RGB (much faster than calling image.getRGB())
+            BufferedImage tmp = new BufferedImage(bi.getWidth(), bi.getHeight(), 
+                    opaque ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB);
+            Graphics2D g = (Graphics2D)tmp.getGraphics();
+            g.drawImage(bi, null, 0, 0);
+            g.dispose();
+            bi = tmp;
+        }
+        // Retrieve image bits
+        int [] imageBits = (int [])bi.getRaster().getDataElements(0, 0, bi.getWidth(), bi.getHeight(), null);
+        // EP : End of modification
+        
         int width = bi.getWidth();
         int height = bi.getHeight();
         byte[] pixels = new byte[4 * width * height];
         for (int y = 0, index = 0; y < height; y++) {
             for (int x = 0; x < width; x++, index += 4) {
-                int argb = bi.getRGB(x, height - 1 - y);
+                // EP : Retrieved image data with raster data 
+                // int argb = bi.getRGB(x, height - 1 - y);
+                int argb = imageBits [x + (height - 1 - y) * width];                
+                // EP : End of modification
                 pixels[index + 0] = (byte) (argb >> 16);
                 pixels[index + 1] = (byte) (argb >> 8);
                 pixels[index + 2] = (byte) argb;
-                pixels[index + 3] = (byte) (argb >> 24);
+                // EP : Added opaque transparency  
+                pixels[index + 3] = opaque ? (byte)0xFF : (byte) (argb >> 24);
+                // EP : End of modification 
             }
         }
         if (!isLinear) {
--- a/src/org/sunflow/image/readers/TGABitmapReader.java
+++ b/src/org/sunflow/image/readers/TGABitmapReader.java
@@ -4,6 +4,8 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import org.sunflow.image.Bitmap;
 import org.sunflow.image.BitmapReader;
@@ -16,7 +18,19 @@
     private static final int[] CHANNEL_INDEX = { 2, 1, 0, 3 };
 
     public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException {
-        InputStream f = new BufferedInputStream(new FileInputStream(filename));
+        // EP : Try to read filename as an URL or as a file
+        InputStream f;
+        try {
+          // Let's try first to read filename as an URL
+          f = new URL(filename).openStream();
+        } catch (MalformedURLException ex) {
+          // Let's try to read filename as a file
+          f = new FileInputStream(filename);
+        }
+
+        f = new BufferedInputStream(f);
+        // End of modification
+        
         byte[] read = new byte[4];
 
         // read header
--- a/src/org/sunflow/PluginRegistry.java
+++ b/src/org/sunflow/PluginRegistry.java
@@ -309,6 +309,8 @@
         bitmapReaderPlugins.registerPlugin("jpg", JPGBitmapReader.class);
         bitmapReaderPlugins.registerPlugin("bmp", BMPBitmapReader.class);
         bitmapReaderPlugins.registerPlugin("igi", IGIBitmapReader.class);
+        // EP : Added extension jpeg
+        bitmapReaderPlugins.registerPlugin("jpeg", JPGBitmapReader.class);
     }
 
     static {
--- a/src/org/sunflow/SunflowAPI.java
+++ b/src/org/sunflow/SunflowAPI.java
@@ -26,6 +26,7 @@
 import org.sunflow.core.SceneParser;
 import org.sunflow.core.Shader;
 import org.sunflow.core.Tesselatable;
+import org.sunflow.core.TextureCache;
 import org.sunflow.core.ParameterList.InterpolationType;
 import org.sunflow.image.ColorFactory;
 import org.sunflow.image.ColorFactory.ColorSpecificationException;
@@ -697,4 +698,12 @@
     public void currentFrame(int currentFrame) {
         this.currentFrame = currentFrame;
     }
+
+    // EP : Made texture cache local to a SunFlow API instance
+    private TextureCache textureCache = new TextureCache();
+    
+    public TextureCache getTextureCache() {
+        return this.textureCache;
+    }
+    // EP : End of modification
 }
\ No newline at end of file
--- a/src/SunflowGUI.java
+++ b/src/SunflowGUI.java
@@ -43,7 +43,6 @@
 import org.sunflow.RealtimeBenchmark;
 import org.sunflow.SunflowAPI;
 import org.sunflow.core.Display;
-import org.sunflow.core.TextureCache;
 import org.sunflow.core.accel.KDTree;
 import org.sunflow.core.display.FileDisplay;
 import org.sunflow.core.display.FrameDisplay;
@@ -52,9 +51,9 @@
 import org.sunflow.system.ImagePanel;
 import org.sunflow.system.Timer;
 import org.sunflow.system.UI;
-import org.sunflow.system.UserInterface;
 import org.sunflow.system.UI.Module;
 import org.sunflow.system.UI.PrintLevel;
+import org.sunflow.system.UserInterface;
 
 @SuppressWarnings("serial")
 public class SunflowGUI extends javax.swing.JFrame implements UserInterface {
@@ -1069,7 +1068,8 @@
     }
 
     private void textureCacheClearMenuItemActionPerformed(ActionEvent evt) {
-        TextureCache.flush();
+        // EP : Made texture cache local to SunFlow API
+        api.getTextureCache().flush();
     }
 
     private void smallTrianglesMenuItemActionPerformed(ActionEvent evt) {