1. Michael Ludwig
  2. ferox

Commits

Michael Ludwig  committed 206def8

Correct light package controllers and results.

  • Participants
  • Parent commits 421d47b
  • Branches default

Comments (0)

Files changed (6)

File ferox-scene/src/main/java/com/ferox/scene/controller/BuildVisibilityIndexTask.java

View file
     private Renderable renderable;
     private ComponentIterator iterator;
 
+    // FIXME just create it?
     public BuildVisibilityIndexTask(SpatialIndex<Entity> index) {
         this.index = index;
     }

File ferox-scene/src/main/java/com/ferox/scene/controller/light/ComputeLightGroupTask.java

View file
+/*
+ * Ferox, a graphics and game library in Java
+ *
+ * Copyright (c) 2012, Michael Ludwig
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *     Redistributions of source code must retain the above copyright notice,
+ *         this list of conditions and the following disclaimer.
+ *     Redistributions in binary form must reproduce the above copyright notice,
+ *         this list of conditions and the following disclaimer in the
+ *         documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.ferox.scene.controller.light;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.ferox.math.AxisAlignedBox;
+import com.ferox.math.Const;
+import com.ferox.math.Matrix4;
+import com.ferox.math.bounds.BoundedSpatialIndex;
+import com.ferox.math.bounds.QuadTree;
+import com.ferox.math.bounds.QueryCallback;
+import com.ferox.scene.AmbientLight;
+import com.ferox.scene.Camera;
+import com.ferox.scene.DirectionLight;
+import com.ferox.scene.InfluenceRegion;
+import com.ferox.scene.Influences;
+import com.ferox.scene.Light;
+import com.ferox.scene.PointLight;
+import com.ferox.scene.Renderable;
+import com.ferox.scene.SpotLight;
+import com.ferox.scene.Transform;
+import com.ferox.scene.controller.BoundsResult;
+import com.ferox.scene.controller.PVSResult;
+import com.ferox.util.Bag;
+import com.lhkbob.entreri.Component;
+import com.lhkbob.entreri.ComponentData;
+import com.lhkbob.entreri.ComponentIterator;
+import com.lhkbob.entreri.Entity;
+import com.lhkbob.entreri.EntitySystem;
+import com.lhkbob.entreri.property.IntProperty;
+import com.lhkbob.entreri.task.Job;
+import com.lhkbob.entreri.task.ParallelAware;
+import com.lhkbob.entreri.task.Task;
+
+public class ComputeLightGroupTask implements Task, ParallelAware {
+    private static final Set<Class<? extends ComponentData<?>>> COMPONENTS;
+    static {
+        Set<Class<? extends ComponentData<?>>> types = new HashSet<Class<? extends ComponentData<?>>>();
+        types.add(Influences.class);
+        types.add(InfluenceRegion.class);
+        types.add(AmbientLight.class);
+        types.add(DirectionLight.class);
+        types.add(SpotLight.class);
+        types.add(PointLight.class);
+        types.add(Renderable.class);
+        types.add(Transform.class);
+        COMPONENTS = Collections.unmodifiableSet(types);
+    }
+
+    // read-only so threadsafe
+    private static final Matrix4 DEFAULT_MAT = new Matrix4().setIdentity();
+    private static final AxisAlignedBox DEFAULT_AABB = new AxisAlignedBox();
+
+    private final BoundedSpatialIndex<LightSource> lightIndex;
+    private IntProperty assignments;
+
+    // results
+    private final List<Bag<Entity>> allVisibleSets;
+    private AxisAlignedBox worldBounds;
+
+    // shared local variables for GC performance
+    private Transform transform;
+    private Influences influenceSet;
+    private InfluenceRegion influenceRegion;
+    private AmbientLight ambient;
+    private DirectionLight direction;
+    private SpotLight spot;
+    private PointLight point;
+
+    public ComputeLightGroupTask() {
+        this.lightIndex = new QuadTree<LightSource>(new AxisAlignedBox(), 2);
+        allVisibleSets = new ArrayList<Bag<Entity>>();
+    }
+
+    @Override
+    public void reset(EntitySystem system) {
+        if (assignments == null) {
+            assignments = system.decorate(Renderable.class, new IntProperty.Factory(-1));
+
+            transform = system.createDataInstance(Transform.class);
+            influenceSet = system.createDataInstance(Influences.class);
+            influenceRegion = system.createDataInstance(InfluenceRegion.class);
+            ambient = system.createDataInstance(AmbientLight.class);
+            direction = system.createDataInstance(DirectionLight.class);
+            spot = system.createDataInstance(SpotLight.class);
+            point = system.createDataInstance(PointLight.class);
+        }
+
+        allVisibleSets.clear();
+        worldBounds = null;
+        lightIndex.clear(true);
+        Arrays.fill(assignments.getIndexedData(), -1);
+    }
+
+    private <T extends Light<T>> void convertToLightSources(T light,
+                                                            EntitySystem system,
+                                                            LightInfluence.Factory<T> factory,
+                                                            List<LightSource> globalLights,
+                                                            List<LightSource> allLights) {
+        ComponentIterator dlt = new ComponentIterator(system).addRequired(light)
+                                                             .addOptional(transform)
+                                                             .addOptional(influenceSet)
+                                                             .addOptional(influenceRegion);
+        while (dlt.next()) {
+            // we don't take advantage of some light types requiring a transform,
+            // because we process ambient lights with this same code
+            Matrix4 t = (transform.isEnabled() ? transform.getMatrix() : DEFAULT_MAT);
+            AxisAlignedBox bounds = null;
+            boolean invertBounds = false;
+
+            if (influenceRegion.isEnabled()) {
+                bounds = new AxisAlignedBox().transform(influenceRegion.getBounds(), t);
+                invertBounds = influenceRegion.isNegated();
+            }
+
+            LightSource l = new LightSource(allLights.size(),
+                                            light.getComponent(),
+                                            factory.create(light, t),
+                                            (influenceSet.isEnabled() ? influenceSet.getInfluencedSet() : null),
+                                            bounds,
+                                            invertBounds);
+
+            if (bounds != null && !invertBounds) {
+                // this light is not a globally influencing light so add it to the index
+                if (!lightIndex.add(l, bounds)) {
+                    // fallback if we're out of bounds
+                    globalLights.add(l);
+                }
+            } else {
+                globalLights.add(l);
+            }
+
+            // always add it to the full list
+            allLights.add(l);
+        }
+    }
+
+    private void queryGlobalLights(Entity e, LightCallback callback,
+                                   List<LightSource> globalLights) {
+        // accumulate globally influencing lights into bit set
+        int numGlobalLights = globalLights.size();
+        for (int i = 0; i < numGlobalLights; i++) {
+            LightSource light = globalLights.get(i);
+
+            // check influence region of light
+            if (light.bounds != null) {
+                if (light.invertBounds) {
+                    // skip light if entity is contained entirely in light bounds
+                    if (light.bounds.contains(callback.entityBounds)) {
+                        continue;
+                    }
+                } else {
+                    // skip light if entity does not intersect light bounds
+                    if (!light.bounds.intersects(callback.entityBounds)) {
+                        continue;
+                    }
+                }
+            }
+
+            // check influence set of light
+            if (light.validEntities != null && !light.validEntities.contains(e)) {
+                continue;
+            }
+
+            // final check
+            if (light.influence.influences(callback.entityBounds)) {
+                // passed the last check, so the entity is influence by the ith light
+                callback.lights.set(light.id);
+            }
+        }
+    }
+
+    /*
+     * This method makes excessive use of raw types, but it is safe given that
+     * TypeIds are used correctly. Java's type inference is just not good enough
+     * to keep track of it.
+     */
+    @Override
+    public Task process(EntitySystem system, Job job) {
+        lightIndex.setExtent(worldBounds);
+
+        // collect all lights
+        List<LightSource> allLights = new ArrayList<LightSource>();
+        List<LightSource> globalLights = new ArrayList<LightSource>();
+        convertToLightSources(direction, system,
+                              GlobalLightInfluence.<DirectionLight> factory(),
+                              globalLights, allLights);
+        convertToLightSources(ambient, system,
+                              GlobalLightInfluence.<AmbientLight> factory(),
+                              globalLights, allLights);
+        convertToLightSources(spot, system, SpotLightInfluence.factory(), globalLights,
+                              allLights);
+        convertToLightSources(point, system, PointLightInfluence.factory(), globalLights,
+                              allLights);
+
+        int groupId = 0;
+        Map<BitSet, Integer> groups = new HashMap<BitSet, Integer>();
+
+        LightCallback callback = new LightCallback(system, allLights.size());
+
+        // process every visible entity
+        for (Bag<Entity> pvs : allVisibleSets) {
+            for (Entity entity : pvs) {
+                // reset callback for this entity
+                callback.set(entity);
+
+                // check if we've already processed this entity in another pvs
+                if (assignments.get(callback.renderable.getIndex()) >= 0) {
+                    continue;
+                }
+
+                queryGlobalLights(entity, callback, globalLights);
+                // FIXME must look into performance cost of this part of the callback,
+                // some small evidence suggests that just the re-working to use
+                // fewer component fetches is significantly faster, I'm not sure why
+                lightIndex.query(callback.entityBounds, callback);
+
+                // light influence bit set is complete for the entity
+                Integer lightGroup = groups.get(callback.lights);
+                if (lightGroup == null) {
+                    // new group encountered
+                    lightGroup = groupId++;
+                    groups.put((BitSet) callback.lights.clone(), lightGroup);
+                }
+
+                // assign group to entity
+                assignments.set(lightGroup.intValue(), callback.renderable.getIndex());
+            }
+        }
+
+        // convert computed groups into LightGroupResult
+        List<Set<Component<? extends Light<?>>>> finalGroups = new ArrayList<Set<Component<? extends Light<?>>>>(groups.size());
+        for (int i = 0; i < groups.size(); i++) {
+            // must fill with nulls up to the full size so that the random
+            // sets below don't cause index oob exceptions
+            finalGroups.add(null);
+        }
+
+        for (Entry<BitSet, Integer> group : groups.entrySet()) {
+            BitSet groupAsBitSet = group.getKey();
+            Set<Component<? extends Light<?>>> lightsInGroup = new HashSet<Component<? extends Light<?>>>();
+            for (int i = groupAsBitSet.nextSetBit(0); i >= 0; i = groupAsBitSet.nextSetBit(i + 1)) {
+                lightsInGroup.add(allLights.get(i).source);
+            }
+
+            // store in array
+            finalGroups.set(group.getValue(), Collections.unmodifiableSet(lightsInGroup));
+        }
+
+        job.report(new LightGroupResult(finalGroups, assignments));
+
+        return null;
+    }
+
+    public void report(PVSResult pvs) {
+        if (pvs.getSource().getType().equals(Camera.class)) {
+            // we are only interested in entities that will be rendered
+            // to a surface, and not for something like a shadow map
+            allVisibleSets.add(pvs.getPotentiallyVisibleSet());
+        }
+    }
+
+    public void report(BoundsResult bounds) {
+        worldBounds = bounds.getBounds();
+    }
+
+    private static class LightCallback implements QueryCallback<LightSource> {
+        final BitSet lights;
+        final Renderable renderable;
+        AxisAlignedBox entityBounds;
+
+        public LightCallback(EntitySystem system, int numLights) {
+            lights = new BitSet(numLights);
+            renderable = system.createDataInstance(Renderable.class);
+            entityBounds = DEFAULT_AABB;
+        }
+
+        public void set(Entity e) {
+            if (e.get(renderable)) {
+                entityBounds = renderable.getWorldBounds();
+            } else {
+                entityBounds = DEFAULT_AABB;
+            }
+            lights.clear();
+        }
+
+        @Override
+        public void process(LightSource item, @Const AxisAlignedBox lightBounds) {
+            if (item.influence.influences(entityBounds)) {
+                // record light
+                lights.set(item.id);
+            }
+        }
+    }
+
+    private static class LightSource {
+        final int id;
+        final Component<? extends Light<?>> source;
+        final LightInfluence influence;
+
+        // non-null only if Influences is present
+        final Set<Entity> validEntities;
+
+        // non-null only if InfluenceRegion is present
+        final AxisAlignedBox bounds;
+        final boolean invertBounds;
+
+        public LightSource(int id, Component<? extends Light<?>> source,
+                           LightInfluence influence, Set<Entity> validEntities,
+                           AxisAlignedBox bounds, boolean invertBounds) {
+            this.id = id;
+            this.source = source;
+            this.influence = influence;
+            this.validEntities = validEntities;
+            this.bounds = bounds;
+            this.invertBounds = invertBounds;
+        }
+    }
+
+    @Override
+    public Set<Class<? extends ComponentData<?>>> getAccessedComponents() {
+        return COMPONENTS;
+    }
+
+    @Override
+    public boolean isEntitySetModified() {
+        return false;
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/light/ComputeShadowFrustumTask.java

View file
+/*
+ * Ferox, a graphics and game library in Java
+ *
+ * Copyright (c) 2012, Michael Ludwig
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *     Redistributions of source code must retain the above copyright notice,
+ *         this list of conditions and the following disclaimer.
+ *     Redistributions in binary form must reproduce the above copyright notice,
+ *         this list of conditions and the following disclaimer in the
+ *         documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.ferox.scene.controller.light;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.ferox.math.AxisAlignedBox;
+import com.ferox.math.Const;
+import com.ferox.math.Matrix4;
+import com.ferox.math.Vector3;
+import com.ferox.math.bounds.Frustum;
+import com.ferox.scene.Camera;
+import com.ferox.scene.DirectionLight;
+import com.ferox.scene.SpotLight;
+import com.ferox.scene.Transform;
+import com.ferox.scene.controller.BoundsResult;
+import com.ferox.scene.controller.FrustumResult;
+import com.lhkbob.entreri.ComponentData;
+import com.lhkbob.entreri.ComponentIterator;
+import com.lhkbob.entreri.EntitySystem;
+import com.lhkbob.entreri.task.Job;
+import com.lhkbob.entreri.task.ParallelAware;
+import com.lhkbob.entreri.task.Task;
+
+public class ComputeShadowFrustumTask implements Task, ParallelAware {
+    private static final Set<Class<? extends ComponentData<?>>> COMPONENTS;
+    static {
+        Set<Class<? extends ComponentData<?>>> types = new HashSet<Class<? extends ComponentData<?>>>();
+        types.add(DirectionLight.class);
+        types.add(SpotLight.class);
+        types.add(Transform.class);
+        COMPONENTS = Collections.unmodifiableSet(types);
+    }
+
+    private AxisAlignedBox sceneBounds;
+    private FrustumResult camera;
+
+    // cached local variables
+    private DirectionLight directionLight;
+    private SpotLight spotLight;
+    private Transform transform;
+
+    private ComponentIterator directionIterator;
+    private ComponentIterator spotIterator;
+
+    public void report(BoundsResult r) {
+        sceneBounds = r.getBounds();
+    }
+
+    public void report(FrustumResult fr) {
+        if (fr.getSource().getType().equals(Camera.class)) {
+            // keep the frustum that has the biggest volume, but most
+            // likely there will only be one camera ever
+            if (camera == null || frustumVolume(camera.getFrustum()) < frustumVolume(fr.getFrustum())) {
+                camera = fr;
+            }
+        }
+    }
+
+    private double frustumVolume(Frustum f) {
+        double dx = Math.abs(f.getFrustumRight() - f.getFrustumLeft());
+        double dy = Math.abs(f.getFrustumTop() - f.getFrustumBottom());
+        double dz = Math.abs(f.getFrustumFar() - f.getFrustumNear());
+        return dx * dy * dz;
+    }
+
+    @Override
+    public void reset(EntitySystem system) {
+        if (directionLight == null) {
+            directionLight = system.createDataInstance(DirectionLight.class);
+            spotLight = system.createDataInstance(SpotLight.class);
+            transform = system.createDataInstance(Transform.class);
+
+            directionIterator = new ComponentIterator(system).addRequired(directionLight)
+                                                             .addRequired(transform);
+            spotIterator = new ComponentIterator(system).addRequired(spotLight)
+                                                        .addRequired(transform);
+        }
+
+        sceneBounds = null;
+        camera = null;
+        directionIterator.reset();
+        spotIterator.reset();
+    }
+
+    @Override
+    public Task process(EntitySystem system, Job job) {
+        if (camera == null) {
+            // if there's nothing being rendered into, then there's no point in
+            // rendering shadows (and we don't have the info to compute a DL
+            // frustum anyway).
+            return null;
+        }
+
+        // Process DirectionLights
+        while (directionIterator.next()) {
+            if (directionLight.isShadowCaster()) {
+                Frustum smFrustum = computeFrustum(directionLight, transform);
+                job.report(new FrustumResult(directionLight.getComponent(), smFrustum));
+            }
+        }
+
+        // Process SpotLights
+        while (spotIterator.next()) {
+            if (spotLight.isShadowCaster()) {
+                Frustum smFrustum = computeFrustum(spotLight, transform);
+                job.report(new FrustumResult(spotLight.getComponent(), smFrustum));
+            }
+        }
+
+        return null;
+    }
+
+    private Frustum computeFrustum(DirectionLight light, Transform t) {
+        // Implement basic frustum improvement techniques from:
+        // http://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx
+        Frustum v = camera.getFrustum();
+        //        Surface s = ((Component<Camera>) camera.getSource()).getData().getSurface();
+        Matrix4 lightMatrix = t.getMatrix();
+
+        Frustum f = new Frustum(true, -1, 1, -1, 1, -1, 1);
+        f.setOrientation(new Vector3(), lightMatrix.getUpperMatrix());
+
+        Matrix4 toLight = f.getViewMatrix();
+
+        // We transform the view frustum into light space and compute the min
+        // and max x/y values of its 8 corners in light space
+        AxisAlignedBox extent = computeViewBounds(toLight, v);
+
+        // If we have scene bounds, tighten the near and far planes to be the
+        // z-projections of the bounds in light space
+        if (sceneBounds != null) {
+            clampToBounds(toLight, sceneBounds, extent);
+        }
+
+        // Update values to fix texel shimmering
+        // FIXME this doesn't work, the main problem is that rotations of the camera can change
+        // the ratio of texel space to world space changes, which causes shimmering
+        // even if we floor the extents. The solution is apparenty to use a bounding
+        // sphere over the 8 points of the view frustum so its size is rotation
+        // invariant
+        double worldArea = (v.getFrustumRight() - v.getFrustumLeft()) * (v.getFrustumTop() - v.getFrustumBottom());
+        double worldUnitsPerTexel = worldArea / (1024 * 1024); // FIXME buffer size
+
+        extent.min.x = Math.floor(extent.min.x / worldUnitsPerTexel) * worldUnitsPerTexel;
+        extent.min.y = Math.floor(extent.min.y / worldUnitsPerTexel) * worldUnitsPerTexel;
+        extent.max.x = Math.floor(extent.max.x / worldUnitsPerTexel) * worldUnitsPerTexel;
+        extent.max.y = Math.floor(extent.max.y / worldUnitsPerTexel) * worldUnitsPerTexel;
+
+        f.setFrustum(true, extent.min.x, extent.max.x, extent.min.y, extent.max.y,
+                     extent.min.z, extent.max.z);
+
+        return f;
+    }
+
+    private void clampToBounds(@Const Matrix4 lightMatrix, @Const AxisAlignedBox scene,
+                               AxisAlignedBox lightExtent) {
+        Vector3[] sceneCorners = new Vector3[8];
+        for (int i = 0; i < sceneCorners.length; i++) {
+            Vector3 c = new Vector3();
+            c.x = (i & 0x1) == 0 ? scene.min.x : scene.max.x;
+            c.y = (i & 0x2) == 0 ? scene.min.y : scene.max.y;
+            c.z = (i & 0x4) == 0 ? scene.min.z : scene.max.z;
+            c.transform(lightMatrix, c);
+            sceneCorners[i] = c;
+        }
+
+        // 12 edges
+        int[] e1 = new int[] {0, 0, 0, 1, 1, 2, 2, 3, 4, 4, 5, 6};
+        int[] e2 = new int[] {1, 2, 4, 3, 5, 3, 6, 7, 5, 6, 7, 7};
+
+        double[] intersections = new double[12 * 4];
+        for (int i = 0; i < e1.length; i++) {
+            intersections[i * 4 + 0] = intersection(sceneCorners[e1[i]].x,
+                                                    sceneCorners[e1[i]].z,
+                                                    sceneCorners[e2[i]].x,
+                                                    sceneCorners[e2[i]].z,
+                                                    lightExtent.min.x);
+            intersections[i * 4 + 1] = intersection(sceneCorners[e1[i]].x,
+                                                    sceneCorners[e1[i]].z,
+                                                    sceneCorners[e2[i]].x,
+                                                    sceneCorners[e2[i]].z,
+                                                    lightExtent.max.x);
+            intersections[i * 4 + 2] = intersection(sceneCorners[e1[i]].y,
+                                                    sceneCorners[e1[i]].z,
+                                                    sceneCorners[e2[i]].y,
+                                                    sceneCorners[e2[i]].z,
+                                                    lightExtent.min.y);
+            intersections[i * 4 + 3] = intersection(sceneCorners[e1[i]].y,
+                                                    sceneCorners[e1[i]].z,
+                                                    sceneCorners[e2[i]].y,
+                                                    sceneCorners[e2[i]].z,
+                                                    lightExtent.max.y);
+        }
+
+        lightExtent.min.z = Double.POSITIVE_INFINITY;
+        lightExtent.max.z = Double.NEGATIVE_INFINITY;
+
+        for (int i = 0; i < intersections.length; i++) {
+            if (Double.isNaN(intersections[i])) {
+                // edge intersection outside of bounds, so fallback to vertex
+                lightExtent.min.z = Math.min(lightExtent.min.z, sceneCorners[e1[i / 4]].z);
+                lightExtent.max.z = Math.max(lightExtent.max.z, sceneCorners[e1[i / 4]].z);
+                lightExtent.min.z = Math.min(lightExtent.min.z, sceneCorners[e2[i / 4]].z);
+                lightExtent.max.z = Math.max(lightExtent.max.z, sceneCorners[e2[i / 4]].z);
+            } else {
+                lightExtent.min.z = Math.min(lightExtent.min.z, intersections[i]);
+                lightExtent.max.z = Math.max(lightExtent.max.z, intersections[i]);
+            }
+        }
+    }
+
+    private double intersection(double ex1, double ey1, double ex2, double ey2, double lx) {
+        if (lx < Math.min(ex1, ex2) || lx > Math.max(ex1, ex2)) {
+            // out of valid intersection range
+            return Double.NaN;
+        } else {
+            // application of slope-intersection, here e1 and e2 are two-dimensional
+            // points in the projected plane of the current test (i.e. xz or yz)
+            return (ey2 - ey1) / (ex2 - ex1) * (lx - ex1) + ey1;
+        }
+    }
+
+    private AxisAlignedBox computeViewBounds(@Const Matrix4 lightMatrix, Frustum f) {
+        AxisAlignedBox extent = new AxisAlignedBox(new Vector3(Double.POSITIVE_INFINITY,
+                                                               Double.POSITIVE_INFINITY,
+                                                               Double.POSITIVE_INFINITY),
+                                                   new Vector3(Double.NEGATIVE_INFINITY,
+                                                               Double.NEGATIVE_INFINITY,
+                                                               Double.NEGATIVE_INFINITY));
+
+        for (int i = 0; i < 8; i++) {
+            Vector3 c = new Vector3();
+            c.x = (i & 0x1) == 0 ? f.getFrustumLeft() : f.getFrustumRight();
+            c.y = (i & 0x2) == 0 ? f.getFrustumBottom() : f.getFrustumTop();
+            c.z = (i & 0x4) == 0 ? f.getFrustumNear() : f.getFrustumFar();
+            if (!f.isOrthogonalProjection()) {
+                c.x *= c.z / f.getFrustumNear();
+                c.y *= c.z / f.getFrustumNear();
+            }
+
+            Vector3 w = new Vector3(f.getLocation());
+            w.addScaled(c.x, new Vector3().cross(f.getDirection(), f.getUp()).normalize())
+             .addScaled(c.y, f.getUp()).addScaled(c.z, f.getDirection());
+            w.transform(lightMatrix, w);
+
+            extent.min.x = Math.min(w.x, extent.min.x);
+            extent.max.x = Math.max(w.x, extent.max.x);
+            extent.min.y = Math.min(w.y, extent.min.y);
+            extent.max.y = Math.max(w.y, extent.max.y);
+            extent.min.z = Math.min(w.z, extent.min.z);
+            extent.max.z = Math.max(w.z, extent.max.z);
+        }
+
+        return extent;
+    }
+
+    private Frustum computeFrustum(SpotLight light, Transform t) {
+        // clamp near and far planes to the falloff distance if possible, 
+        // otherwise select a depth range that likely will not cause any problems
+        double near = (light.getFalloffDistance() > 0 ? Math.min(.1 * light.getFalloffDistance(),
+                                                                 .1) : .1);
+        double far = (light.getFalloffDistance() > 0 ? light.getFalloffDistance() : 1000);
+        Frustum f = new Frustum(light.getCutoffAngle() * 2, 1.0, near, far);
+        f.setOrientation(t.getMatrix());
+        return f;
+    }
+
+    @Override
+    public Set<Class<? extends ComponentData<?>>> getAccessedComponents() {
+        return COMPONENTS;
+    }
+
+    @Override
+    public boolean isEntitySetModified() {
+        return false;
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/light/LightGroupController.java

-/*
- * Ferox, a graphics and game library in Java
- *
- * Copyright (c) 2012, Michael Ludwig
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- *     Redistributions of source code must retain the above copyright notice,
- *         this list of conditions and the following disclaimer.
- *     Redistributions in binary form must reproduce the above copyright notice,
- *         this list of conditions and the following disclaimer in the
- *         documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.ferox.scene.controller.light;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import com.ferox.math.AxisAlignedBox;
-import com.ferox.math.Const;
-import com.ferox.math.Matrix4;
-import com.ferox.math.bounds.QuadTree;
-import com.ferox.math.bounds.QueryCallback;
-import com.ferox.math.bounds.SpatialIndex;
-import com.ferox.scene.AmbientLight;
-import com.ferox.scene.Camera;
-import com.ferox.scene.DirectionLight;
-import com.ferox.scene.InfluenceRegion;
-import com.ferox.scene.Influences;
-import com.ferox.scene.Light;
-import com.ferox.scene.PointLight;
-import com.ferox.scene.Renderable;
-import com.ferox.scene.SpotLight;
-import com.ferox.scene.Transform;
-import com.ferox.scene.controller.PVSResult;
-import com.ferox.util.Bag;
-import com.lhkbob.entreri.Component;
-import com.lhkbob.entreri.ComponentIterator;
-import com.lhkbob.entreri.Entity;
-import com.lhkbob.entreri.EntitySystem;
-import com.lhkbob.entreri.Result;
-import com.lhkbob.entreri.SimpleController;
-import com.lhkbob.entreri.TypeId;
-import com.lhkbob.entreri.property.IntProperty;
-
-public class LightGroupController extends SimpleController {
-    // read-only so threadsafe
-    private static final Matrix4 DEFAULT_MAT = new Matrix4().setIdentity();
-    private static final AxisAlignedBox DEFAULT_AABB = new AxisAlignedBox();
-
-    private SpatialIndex<LightSource> lightIndex;
-
-    private IntProperty assignments;
-    private List<Bag<Entity>> allVisibleSets;
-
-    public LightGroupController(@Const AxisAlignedBox worldBounds) {
-        this.lightIndex = new QuadTree<LightSource>(worldBounds, 1);
-    }
-
-    private <T extends Light<T>> void processLights(TypeId<T> id,
-                                                    LightInfluence.Factory<T> factory,
-                                                    List<LightSource> globalLights,
-                                                    List<LightSource> allLights) {
-        Transform transform = getEntitySystem().createDataInstance(Transform.ID);
-        T light = getEntitySystem().createDataInstance(id);
-        Influences influenceSet = getEntitySystem().createDataInstance(Influences.ID);
-        InfluenceRegion region = getEntitySystem().createDataInstance(InfluenceRegion.ID);
-
-        ComponentIterator dlt = new ComponentIterator(getEntitySystem()).addRequired(light)
-                                                                        .addOptional(transform)
-                                                                        .addOptional(influenceSet)
-                                                                        .addOptional(region);
-        while (dlt.next()) {
-            Matrix4 t = (transform.isEnabled() ? transform.getMatrix() : DEFAULT_MAT);
-            AxisAlignedBox bounds = null;
-            boolean invertBounds = false;
-
-            if (region.isEnabled()) {
-                bounds = new AxisAlignedBox().transform(region.getBounds(), t);
-                invertBounds = region.isNegated();
-            }
-
-            LightSource l = new LightSource(allLights.size(),
-                                            light.getComponent(),
-                                            factory.create(light, t),
-                                            (influenceSet.isEnabled() ? influenceSet.getInfluencedSet() : null),
-                                            bounds,
-                                            invertBounds);
-
-            if (bounds != null && !invertBounds) {
-                // this light is not a globally influencing light so add it to the index
-                if (!lightIndex.add(l, bounds)) {
-                    // fallback if we're out of bounds
-                    globalLights.add(l);
-                }
-            } else {
-                globalLights.add(l);
-            }
-
-            // always add it to the full list
-            allLights.add(l);
-        }
-    }
-
-    private void queryGlobalLights(Entity e, LightCallback callback,
-                                   List<LightSource> globalLights) {
-        // accumulate globally influencing lights into bit set
-        int numGlobalLights = globalLights.size();
-        for (int i = 0; i < numGlobalLights; i++) {
-            LightSource light = globalLights.get(i);
-
-            // check influence region of light
-            if (light.bounds != null) {
-                if (light.invertBounds) {
-                    // skip light if entity is contained entirely in light bounds
-                    if (light.bounds.contains(callback.entityBounds)) {
-                        continue;
-                    }
-                } else {
-                    // skip light if entity does not intersect light bounds
-                    if (!light.bounds.intersects(callback.entityBounds)) {
-                        continue;
-                    }
-                }
-            }
-
-            // check influence set of light
-            if (light.validEntities != null && !light.validEntities.contains(e)) {
-                continue;
-            }
-
-            // final check
-            if (light.influence.influences(callback.entityBounds)) {
-                // passed the last check, so the entity is influence by the ith light
-                callback.lights.set(i);
-            }
-        }
-    }
-
-    /*
-     * This method makes excessive use of raw types, but it is safe given that
-     * TypeIds are used correctly. Java's type inference is just not good enough
-     * to keep track of it.
-     */
-    @Override
-    public void process(double dt) {
-        // collect all lights
-        List<LightSource> allLights = new ArrayList<LightSource>();
-        List<LightSource> globalLights = new ArrayList<LightSource>();
-        processLights(DirectionLight.ID, GlobalLightInfluence.<DirectionLight> factory(),
-                      globalLights, allLights);
-        processLights(AmbientLight.ID, GlobalLightInfluence.<AmbientLight> factory(),
-                      globalLights, allLights);
-        processLights(SpotLight.ID, SpotLightInfluence.factory(), globalLights, allLights);
-        processLights(PointLight.ID, PointLightInfluence.factory(), globalLights,
-                      allLights);
-
-        int groupId = 0;
-        Map<BitSet, Integer> groups = new HashMap<BitSet, Integer>();
-
-        LightCallback callback = new LightCallback(getEntitySystem(), allLights.size());
-
-        // process every visible entity
-        for (Bag<Entity> pvs : allVisibleSets) {
-            for (Entity entity : pvs) {
-                // reset callback for this entity
-                callback.set(entity);
-
-                // check if we've already processed this entity in another pvs
-                if (!callback.renderable.isEnabled() || assignments.get(callback.renderable.getIndex()) >= 0) {
-                    continue;
-                }
-
-                queryGlobalLights(entity, callback, globalLights);
-                // FIXME must look into performance cost of this part of the callback,
-                // some small evidence suggests that just the re-working to use
-                // fewer component fetches is significantly faster, I'm not sure why
-                lightIndex.query(callback.entityBounds, callback);
-
-                // light influence bit set is complete for the entity
-                Integer lightGroup = groups.get(callback.lights);
-                if (lightGroup == null) {
-                    // new group encountered
-                    lightGroup = groupId++;
-                    groups.put((BitSet) callback.lights.clone(), lightGroup);
-                }
-
-                // assign group to entity
-                assignments.set(lightGroup.intValue(), callback.renderable.getIndex());
-            }
-        }
-
-        // convert computed groups into LightGroupResult
-        List<Set<Component<? extends Light<?>>>> finalGroups = new ArrayList<Set<Component<? extends Light<?>>>>(groups.size());
-        for (int i = 0; i < groups.size(); i++) {
-            // must fill with nulls up to the full size so that the random
-            // sets below don't cause index oob exceptions
-            finalGroups.add(null);
-        }
-
-        for (Entry<BitSet, Integer> group : groups.entrySet()) {
-            BitSet groupAsBitSet = group.getKey();
-            Set<Component<? extends Light<?>>> lightsInGroup = new HashSet<Component<? extends Light<?>>>();
-            for (int i = groupAsBitSet.nextSetBit(0); i >= 0; i = groupAsBitSet.nextSetBit(i + 1)) {
-                lightsInGroup.add(allLights.get(i).source);
-            }
-
-            // store in array
-            finalGroups.set(group.getValue(), Collections.unmodifiableSet(lightsInGroup));
-        }
-
-        getEntitySystem().getControllerManager()
-                         .report(new LightGroupResult(finalGroups, assignments));
-    }
-
-    @Override
-    public void preProcess(double dt) {
-        allVisibleSets = new ArrayList<Bag<Entity>>();
-        Arrays.fill(assignments.getIndexedData(), -1);
-        lightIndex.clear(true);
-    }
-
-    @Override
-    public void init(EntitySystem system) {
-        super.init(system);
-        assignments = system.decorate(Renderable.ID, new IntProperty.Factory(-1));
-    }
-
-    @Override
-    public void destroy() {
-        getEntitySystem().undecorate(Renderable.ID, assignments);
-        assignments = null;
-        lightIndex.clear();
-        super.destroy();
-    }
-
-    @Override
-    public void report(Result r) {
-        if (r instanceof PVSResult) {
-            PVSResult pvs = (PVSResult) r;
-            if (pvs.getSource().getTypeId() == Camera.ID) {
-                // we are only interested in entities that will be rendered
-                // to a surface, and not for something like a shadow map
-                allVisibleSets.add(pvs.getPotentiallyVisibleSet());
-            }
-        }
-    }
-
-    private static class LightCallback implements QueryCallback<LightSource> {
-        final BitSet lights;
-        final Renderable renderable;
-        AxisAlignedBox entityBounds;
-
-        public LightCallback(EntitySystem system, int numLights) {
-            lights = new BitSet(numLights);
-            renderable = system.createDataInstance(Renderable.ID);
-            entityBounds = DEFAULT_AABB;
-        }
-
-        public void set(Entity e) {
-            if (e.get(renderable)) {
-                entityBounds = renderable.getWorldBounds();
-            } else {
-                entityBounds = DEFAULT_AABB;
-            }
-            lights.clear();
-        }
-
-        @Override
-        public void process(LightSource item, @Const AxisAlignedBox lightBounds) {
-            if (item.influence.influences(entityBounds)) {
-                // record light
-                lights.set(item.id);
-            }
-        }
-    }
-
-    private static class LightSource {
-        final int id;
-        final Component<? extends Light<?>> source;
-        final LightInfluence influence;
-
-        // non-null only if Influences is present
-        final Set<Entity> validEntities;
-
-        // non-null only if InfluenceRegion is present
-        final AxisAlignedBox bounds;
-        final boolean invertBounds;
-
-        public LightSource(int id, Component<? extends Light<?>> source,
-                           LightInfluence influence, Set<Entity> validEntities,
-                           AxisAlignedBox bounds, boolean invertBounds) {
-            this.id = id;
-            this.source = source;
-            this.influence = influence;
-            this.validEntities = validEntities;
-            this.bounds = bounds;
-            this.invertBounds = invertBounds;
-        }
-    }
-}

File ferox-scene/src/main/java/com/ferox/scene/controller/light/LightGroupResult.java

View file
 
 import com.ferox.scene.Light;
 import com.lhkbob.entreri.Component;
-import com.lhkbob.entreri.Result;
 import com.lhkbob.entreri.property.IntProperty;
+import com.lhkbob.entreri.task.Result;
 
-public class LightGroupResult implements Result {
+public class LightGroupResult extends Result {
     private final IntProperty groupAssignment;
     private final List<Set<Component<? extends Light<?>>>> groups;
 

File ferox-scene/src/main/java/com/ferox/scene/controller/light/ShadowFrustumController.java

-/*
- * Ferox, a graphics and game library in Java
- *
- * Copyright (c) 2012, Michael Ludwig
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- *     Redistributions of source code must retain the above copyright notice,
- *         this list of conditions and the following disclaimer.
- *     Redistributions in binary form must reproduce the above copyright notice,
- *         this list of conditions and the following disclaimer in the
- *         documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.ferox.scene.controller.light;
-
-import com.ferox.math.AxisAlignedBox;
-import com.ferox.math.Const;
-import com.ferox.math.Matrix4;
-import com.ferox.math.Vector3;
-import com.ferox.math.bounds.Frustum;
-import com.ferox.renderer.Surface;
-import com.ferox.scene.Camera;
-import com.ferox.scene.DirectionLight;
-import com.ferox.scene.SpotLight;
-import com.ferox.scene.Transform;
-import com.ferox.scene.controller.FrustumResult;
-import com.ferox.scene.controller.SceneBoundsResult;
-import com.lhkbob.entreri.Component;
-import com.lhkbob.entreri.ComponentIterator;
-import com.lhkbob.entreri.Result;
-import com.lhkbob.entreri.SimpleController;
-
-public class ShadowFrustumController extends SimpleController {
-    private static final Matrix4 DEFAULT_MAT = new Matrix4().setIdentity();
-
-    private SceneBoundsResult sceneBounds;
-    private FrustumResult camera;
-
-    @Override
-    public void report(Result r) {
-        if (r instanceof SceneBoundsResult) {
-            sceneBounds = (SceneBoundsResult) r;
-        } else if (r instanceof FrustumResult) {
-            FrustumResult fr = (FrustumResult) r;
-            if (fr.getSource().getTypeId() == Camera.ID) {
-                // keep the frustum that has the biggest volume, but most
-                // likely there will only be one camera ever
-                if (camera == null || frustumVolume(camera.getFrustum()) < frustumVolume(fr.getFrustum())) {
-                    camera = fr;
-                }
-            }
-        }
-    }
-
-    @Override
-    public void preProcess(double dt) {
-        sceneBounds = null;
-        camera = null;
-    }
-
-    private double frustumVolume(Frustum f) {
-        double dx = Math.abs(f.getFrustumRight() - f.getFrustumLeft());
-        double dy = Math.abs(f.getFrustumTop() - f.getFrustumBottom());
-        double dz = Math.abs(f.getFrustumFar() - f.getFrustumNear());
-        return dx * dy * dz;
-    }
-
-    @Override
-    public void process(double dt) {
-        if (camera == null) {
-            // if there's nothing being rendered into, then there's no point in
-            // rendering shadows (and we don't have the info to compute a DL
-            // frustum anyway).
-            return;
-        }
-
-        // Process DirectionLights
-        DirectionLight dl = getEntitySystem().createDataInstance(DirectionLight.ID);
-        Transform t = getEntitySystem().createDataInstance(Transform.ID);
-        ComponentIterator it = new ComponentIterator(getEntitySystem()).addRequired(dl)
-                                                                       .addOptional(t);
-
-        while (it.next()) {
-            if (dl.isShadowCaster()) {
-                Frustum smFrustum = computeFrustum(dl, t);
-                getEntitySystem().getControllerManager()
-                                 .report(new FrustumResult(dl.getComponent(), smFrustum));
-            }
-        }
-
-        // Process SpotLights
-        SpotLight sl = getEntitySystem().createDataInstance(SpotLight.ID);
-        it = new ComponentIterator(getEntitySystem()).addRequired(sl).addOptional(t);
-
-        while (it.next()) {
-            if (sl.isShadowCaster()) {
-                Frustum smFrustum = computeFrustum(sl, t);
-                getEntitySystem().getControllerManager()
-                                 .report(new FrustumResult(sl.getComponent(), smFrustum));
-            }
-        }
-    }
-
-    private Frustum computeFrustum(DirectionLight light, Transform t) {
-        // Implement basic frustum improvement techniques from:
-        // http://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx
-        Frustum v = camera.getFrustum();
-        Surface s = ((Component<Camera>) camera.getSource()).getData().getSurface();
-        Matrix4 lightMatrix = (t.isEnabled() ? t.getMatrix() : DEFAULT_MAT);
-
-        Frustum f = new Frustum(true, -1, 1, -1, 1, -1, 1);
-        f.setOrientation(new Vector3(), lightMatrix.getUpperMatrix());
-
-        Matrix4 toLight = f.getViewMatrix();
-
-        // We transform the view frustum into light space and compute the min
-        // and max x/y values of its 8 corners in light space
-        AxisAlignedBox extent = computeViewBounds(toLight, v);
-
-        // If we have scene bounds, tighten the near and far planes to be the
-        // z-projections of the bounds in light space
-        if (sceneBounds != null) {
-            clampToBounds(toLight, sceneBounds.getSceneBounds(), extent);
-        }
-
-        // Update values to fix texel shimmering
-        // FIXME this doesn't work, the main problem is that rotations of the camera can change
-        // the ratio of texel space to world space changes, which causes shimmering
-        // even if we floor the extents. The solution is apparenty to use a bounding
-        // sphere over the 8 points of the view frustum so its size is rotation
-        // invariant
-        double worldArea = (v.getFrustumRight() - v.getFrustumLeft()) * (v.getFrustumTop() - v.getFrustumBottom());
-        //        double worldUnitsPerTexel = worldArea / (s.getWidth() * s.getHeight());
-        //        double worldArea = (extent.max.x - extent.min.x) * (extent.max.y - extent.min.y);
-        double worldUnitsPerTexel = worldArea / (1024 * 1024); // FIXME buffer size
-
-        extent.min.x = Math.floor(extent.min.x / worldUnitsPerTexel) * worldUnitsPerTexel;
-        extent.min.y = Math.floor(extent.min.y / worldUnitsPerTexel) * worldUnitsPerTexel;
-        extent.max.x = Math.floor(extent.max.x / worldUnitsPerTexel) * worldUnitsPerTexel;
-        extent.max.y = Math.floor(extent.max.y / worldUnitsPerTexel) * worldUnitsPerTexel;
-
-        //                extent.min.scale(1 / worldUnitsPerTexel);
-        //        extent.min.floor();
-        //                extent.min.scale(worldUnitsPerTexel);
-
-        //                extent.max.scale(1 / worldUnitsPerTexel);
-        //        extent.max.floor();
-        //                extent.max.scale(worldUnitsPerTexel);
-
-        f.setFrustum(true, extent.min.x, extent.max.x, extent.min.y, extent.max.y,
-                     extent.min.z, extent.max.z);
-
-        return f;
-    }
-
-    private void clampToBounds(@Const Matrix4 lightMatrix, @Const AxisAlignedBox scene,
-                               AxisAlignedBox lightExtent) {
-        Vector3[] sceneCorners = new Vector3[8];
-        for (int i = 0; i < sceneCorners.length; i++) {
-            Vector3 c = new Vector3();
-            c.x = (i & 0x1) == 0 ? scene.min.x : scene.max.x;
-            c.y = (i & 0x2) == 0 ? scene.min.y : scene.max.y;
-            c.z = (i & 0x4) == 0 ? scene.min.z : scene.max.z;
-            c.transform(lightMatrix, c);
-            sceneCorners[i] = c;
-        }
-
-        // 12 edges
-        int[] e1 = new int[] {0, 0, 0, 1, 1, 2, 2, 3, 4, 4, 5, 6};
-        int[] e2 = new int[] {1, 2, 4, 3, 5, 3, 6, 7, 5, 6, 7, 7};
-
-        double[] intersections = new double[12 * 4];
-        for (int i = 0; i < e1.length; i++) {
-            intersections[i * 4 + 0] = intersection(sceneCorners[e1[i]].x,
-                                                    sceneCorners[e1[i]].z,
-                                                    sceneCorners[e2[i]].x,
-                                                    sceneCorners[e2[i]].z,
-                                                    lightExtent.min.x);
-            intersections[i * 4 + 1] = intersection(sceneCorners[e1[i]].x,
-                                                    sceneCorners[e1[i]].z,
-                                                    sceneCorners[e2[i]].x,
-                                                    sceneCorners[e2[i]].z,
-                                                    lightExtent.max.x);
-            intersections[i * 4 + 2] = intersection(sceneCorners[e1[i]].y,
-                                                    sceneCorners[e1[i]].z,
-                                                    sceneCorners[e2[i]].y,
-                                                    sceneCorners[e2[i]].z,
-                                                    lightExtent.min.y);
-            intersections[i * 4 + 3] = intersection(sceneCorners[e1[i]].y,
-                                                    sceneCorners[e1[i]].z,
-                                                    sceneCorners[e2[i]].y,
-                                                    sceneCorners[e2[i]].z,
-                                                    lightExtent.max.y);
-        }
-
-        lightExtent.min.z = Double.POSITIVE_INFINITY;
-        lightExtent.max.z = Double.NEGATIVE_INFINITY;
-
-        for (int i = 0; i < intersections.length; i++) {
-            if (Double.isNaN(intersections[i])) {
-                // edge intersection outside of bounds, so fallback to vertex
-                lightExtent.min.z = Math.min(lightExtent.min.z, sceneCorners[e1[i / 4]].z);
-                lightExtent.max.z = Math.max(lightExtent.max.z, sceneCorners[e1[i / 4]].z);
-                lightExtent.min.z = Math.min(lightExtent.min.z, sceneCorners[e2[i / 4]].z);
-                lightExtent.max.z = Math.max(lightExtent.max.z, sceneCorners[e2[i / 4]].z);
-            } else {
-                lightExtent.min.z = Math.min(lightExtent.min.z, intersections[i]);
-                lightExtent.max.z = Math.max(lightExtent.max.z, intersections[i]);
-            }
-        }
-    }
-
-    private double intersection(double ex1, double ey1, double ex2, double ey2, double lx) {
-        if (lx < Math.min(ex1, ex2) || lx > Math.max(ex1, ex2)) {
-            // out of valid intersection range
-            return Double.NaN;
-        } else {
-            // application of slope-intersection, here e1 and e2 are two-dimensional
-            // points in the projected plane of the current test (i.e. xz or yz)
-            return (ey2 - ey1) / (ex2 - ex1) * (lx - ex1) + ey1;
-        }
-    }
-
-    private AxisAlignedBox computeViewBounds(@Const Matrix4 lightMatrix, Frustum f) {
-        AxisAlignedBox extent = new AxisAlignedBox(new Vector3(Double.POSITIVE_INFINITY,
-                                                               Double.POSITIVE_INFINITY,
-                                                               Double.POSITIVE_INFINITY),
-                                                   new Vector3(Double.NEGATIVE_INFINITY,
-                                                               Double.NEGATIVE_INFINITY,
-                                                               Double.NEGATIVE_INFINITY));
-
-        for (int i = 0; i < 8; i++) {
-            Vector3 c = new Vector3();
-            c.x = (i & 0x1) == 0 ? f.getFrustumLeft() : f.getFrustumRight();
-            c.y = (i & 0x2) == 0 ? f.getFrustumBottom() : f.getFrustumTop();
-            c.z = (i & 0x4) == 0 ? f.getFrustumNear() : f.getFrustumFar();
-            if (!f.isOrthogonalProjection()) {
-                c.x *= c.z / f.getFrustumNear();
-                c.y *= c.z / f.getFrustumNear();
-            }
-
-            Vector3 w = new Vector3(f.getLocation());
-            w.addScaled(c.x, new Vector3().cross(f.getDirection(), f.getUp()).normalize())
-             .addScaled(c.y, f.getUp()).addScaled(c.z, f.getDirection());
-            w.transform(lightMatrix, w);
-
-            extent.min.x = Math.min(w.x, extent.min.x);
-            extent.max.x = Math.max(w.x, extent.max.x);
-            extent.min.y = Math.min(w.y, extent.min.y);
-            extent.max.y = Math.max(w.y, extent.max.y);
-            extent.min.z = Math.min(w.z, extent.min.z);
-            extent.max.z = Math.max(w.z, extent.max.z);
-        }
-
-        return extent;
-    }
-
-    private Frustum computeFrustum(SpotLight light, Transform t) {
-        // clamp near and far planes to the falloff distance if possible, 
-        // otherwise select a depth range that likely will not cause any problems
-        double near = (light.getFalloffDistance() > 0 ? Math.min(.1 * light.getFalloffDistance(),
-                                                                 .1) : .1);
-        double far = (light.getFalloffDistance() > 0 ? light.getFalloffDistance() : 1000);
-        Frustum f = new Frustum(light.getCutoffAngle() * 2, 1.0, near, far);
-        if (t.isEnabled()) {
-            f.setOrientation(t.getMatrix());
-        } else {
-            f.setOrientation(DEFAULT_MAT);
-        }
-        return f;
-    }
-}