Commits

Michael Ludwig  committed 6938003

Add a re-implementation of FFP rendering that uses component versions to cache state computation, and uses post-man sorting for improved performance

  • Participants
  • Parent commits 128ca60

Comments (0)

Files changed (18)

File ferox-demos/src/main/java/com/ferox/physics/PhysicsApplicationStub.java

                                      new ComputePVSTask(),
                                      new ComputeLightGroupTask(),
                                      new FixedFunctionRenderController(surface.getFramework()));
+        //                                     new FixedFunctionRenderTask(surface.getFramework(),
+        //                                                                 1024,
+        //                                                                 false));
 
         surface.setVSyncEnabled(true);
 

File ferox-demos/src/main/java/com/ferox/scene/controller/ffp/SimpleTest.java

         Framework framework = (LWJGL ? LwjglFramework.create() : JoglFramework.create());
         OnscreenSurface surface = framework.createSurface(new OnscreenSurfaceOptions().setWidth(800)
                                                                                       .setHeight(600)
-                                                                                      //            .setFullscreenMode(new DisplayMode(1440, 900, PixelFormat.RGB_24BIT))
-                                                                                      //                                                                                      .setMultiSampling(MultiSampling.FOUR_X)
                                                                                       .setResizable(false));
         //        surface.setVSyncEnabled(true);
 
         Entity camera = system.addEntity();
         camera.add(Transform.class)
               .getData()
-              .setMatrix(new Matrix4(-1,
-                                     0,
-                                     0,
-                                     0,
-                                     0,
-                                     1,
-                                     0,
-                                     0,
-                                     0,
-                                     0,
-                                     -1,
-                                     .9 * BOUNDS,
-                                     0,
-                                     0,
-                                     0,
-                                     1));
+              .setMatrix(new Matrix4().set(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1,
+                                           .9 * BOUNDS, 0, 0, 0, 1));
         camera.add(Camera.class).getData().setSurface(surface).setZDistances(0.1, 1200)
               .setFieldOfView(75);
 
             e.add(DiffuseColor.class).getData().setColor(c);
             e.add(Transform.class)
              .getData()
-             .setMatrix(new Matrix4(1,
-                                    0,
-                                    0,
-                                    Math.random() * BOUNDS - BOUNDS / 2,
-                                    0,
-                                    1,
-                                    0,
-                                    Math.random() * BOUNDS - BOUNDS / 2,
-                                    0,
-                                    0,
-                                    1,
-                                    Math.random() * BOUNDS - BOUNDS / 2,
-                                    0,
-                                    0,
-                                    0,
-                                    1));
+             .setMatrix(new Matrix4().set(1, 0, 0, Math.random() * BOUNDS - BOUNDS / 2,
+                                          0, 1, 0, Math.random() * BOUNDS - BOUNDS / 2,
+                                          0, 0, 1, Math.random() * BOUNDS - BOUNDS / 2,
+                                          0, 0, 0, 1));
             e.add(Animation.class);
             totalpolys += polycount;
         }
             }
             light.add(Transform.class)
                  .getData()
-                 .setMatrix(new Matrix4(1,
-                                        0,
-                                        0,
-                                        Math.random() * BOUNDS - BOUNDS / 2,
-                                        0,
-                                        1,
-                                        0,
-                                        Math.random() * BOUNDS - BOUNDS / 2,
-                                        0,
-                                        0,
-                                        1,
-                                        Math.random() * BOUNDS - BOUNDS / 2,
-                                        0,
-                                        0,
-                                        0,
-                                        1));
+                 .setMatrix(new Matrix4().set(1, 0, 0,
+                                              Math.random() * BOUNDS - BOUNDS / 2, 0, 1,
+                                              0, Math.random() * BOUNDS - BOUNDS / 2, 0,
+                                              0, 1, Math.random() * BOUNDS - BOUNDS / 2,
+                                              0, 0, 0, 1));
         }
         system.addEntity().add(AmbientLight.class).getData()
               .setColor(new ColorRGB(0.2, 0.2, 0.2));
                                          new ComputePVSTask(),
                                          new ComputeLightGroupTask(),
                                          new FixedFunctionRenderController(framework));
+        //                                         new FixedFunctionRenderTask(framework,
+        //                                                                     1024,
+        //                                                                     false));
 
         long now = System.nanoTime();
         int numRuns = 0;

File ferox-renderer/ferox-renderer-api/src/main/java/com/ferox/resource/VertexAttribute.java

     public int getMaximumNumVertices() {
         return (buffer.getData().getLength() - offset) / (elementSize + stride);
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof VertexAttribute)) {
+            return false;
+        }
+
+        VertexAttribute v = (VertexAttribute) o;
+        return v.buffer == buffer && v.offset == offset && v.stride == stride && v.elementSize == elementSize;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 17;
+        hash += 31 * buffer.hashCode();
+        hash += 31 * offset;
+        hash += 31 * stride;
+        hash += 31 * elementSize;
+        return hash;
+    }
 }

File ferox-scene/src/main/java/com/ferox/scene/Renderable.java

      * might cause, in world space. A controller or other processor must use
      * this method to keep the world bounds in sync with any changes to the
      * local bounds.
+     * <p>
+     * Note that unlike all other properties of the renderable, setting the
+     * world bounds does not update the version of the component. This is
+     * because the world bounds is dependent on the local bounds (which will
+     * update the version) and the transform (not part of the renderable).
      * 
      * @param bounds The new world bounds of the entity
      * @return This component, for chaining purposes
      */
     public Renderable setWorldBounds(@Const AxisAlignedBox bounds) {
         worldBounds.set(bounds, getIndex());
-        updateVersion();
         return this;
     }
 }

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp/ShadowMapGroupFactory.java

 
 public class ShadowMapGroupFactory implements StateGroupFactory {
     // FIXME must fix auto-formatting ugliness in these situations
-    private static final Matrix4 bias = new Matrix4(.5,
-                                                    0,
-                                                    0,
-                                                    .5,
-                                                    0,
-                                                    .5,
-                                                    0,
-                                                    .5,
-                                                    0,
-                                                    0,
-                                                    .5,
-                                                    .5,
-                                                    0,
-                                                    0,
-                                                    0,
-                                                    1);
+    private static final Matrix4 bias = new Matrix4().set(.5, 0, 0, .5, 0, .5, 0, .5, 0,
+                                                          0, .5, .5, 0, 0, 0, 1);
 
     private final StateGroupFactory childFactory;
     private final ShadowMapCache smCache;

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/CameraState.java

+package com.ferox.scene.controller.ffp2;
+
+import com.ferox.math.bounds.Frustum;
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+
+public class CameraState implements State {
+    private final Frustum camera;
+
+    public CameraState(Frustum camera) {
+        this.camera = camera;
+    }
+
+    public Frustum getFrustum() {
+        return camera;
+    }
+
+    @Override
+    public void visitNode(StateNode currentNode, AppliedEffects effects,
+                          HardwareAccessLayer access) {
+        FixedFunctionRenderer r = access.getCurrentContext().getFixedFunctionRenderer();
+        r.setProjectionMatrix(camera.getProjectionMatrix());
+        r.setModelViewMatrix(camera.getViewMatrix());
+
+        AppliedEffects childEffects = effects.applyViewMatrix(camera.getViewMatrix());
+        currentNode.visitChildren(childEffects, access);
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/ColorState.java

+package com.ferox.scene.controller.ffp2;
+
+import com.ferox.math.ColorRGB;
+import com.ferox.math.Const;
+import com.ferox.math.Vector4;
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+
+public class ColorState implements State {
+    public static final Vector4 DEFAULT_DIFFUSE = new Vector4(0.8, 0.8, 0.8, 1.0);
+    public static final Vector4 DEFAULT_SPECULAR = new Vector4(0.0, 0.0, 0.0, 1.0);
+    public static final Vector4 DEFAULT_EMITTED = new Vector4(0.0, 0.0, 0.0, 1.0);
+    public static final Vector4 DEFAULT_AMBIENT = new Vector4(0.2, 0.2, 0.2, 1.0);
+
+    private final Vector4 diffuse = new Vector4();
+    private final Vector4 specular = new Vector4();
+    private final Vector4 emitted = new Vector4();
+
+    private double shininess;
+
+    public void set(@Const ColorRGB diffuse, @Const ColorRGB specular,
+                    @Const ColorRGB emitted, double alpha, double shininess) {
+        if (diffuse == null) {
+            this.diffuse.set(DEFAULT_DIFFUSE).w = alpha;
+        } else {
+            this.diffuse.set(diffuse.red(), diffuse.green(), diffuse.blue(), alpha);
+        }
+
+        if (specular == null) {
+            this.specular.set(DEFAULT_SPECULAR).w = alpha;
+        } else {
+            this.specular.set(specular.red(), specular.green(), specular.blue(), alpha);
+        }
+
+        if (emitted == null) {
+            this.emitted.set(DEFAULT_EMITTED).w = alpha;
+        } else {
+            this.emitted.set(emitted.red(), emitted.green(), emitted.blue(), alpha);
+        }
+
+        this.shininess = shininess;
+    }
+
+    public double getShininess() {
+        return shininess;
+    }
+
+    @Const
+    public Vector4 getDiffuse() {
+        return diffuse;
+    }
+
+    @Const
+    public Vector4 getSpecular() {
+        return specular;
+    }
+
+    @Const
+    public Vector4 getEmitted() {
+        return emitted;
+    }
+
+    public double getAlpha() {
+        return diffuse.w;
+    }
+
+    @Override
+    public void visitNode(StateNode currentNode, AppliedEffects effects,
+                          HardwareAccessLayer access) {
+        FixedFunctionRenderer r = access.getCurrentContext().getFixedFunctionRenderer();
+
+        r.setMaterial(DEFAULT_AMBIENT, diffuse, specular, emitted);
+        r.setMaterialShininess(shininess);
+
+        currentNode.visitChildren(effects, access);
+    }
+
+    @Override
+    public int hashCode() {
+        long bits = Double.doubleToLongBits(shininess);
+
+        int hash = 17 * (int) (bits ^ (bits >>> 32));
+        hash = 31 * hash + diffuse.hashCode();
+        hash = 31 * hash + emitted.hashCode();
+        hash = 31 * hash + specular.hashCode();
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ColorState)) {
+            return false;
+        }
+
+        ColorState ts = (ColorState) o;
+        return ts.diffuse.equals(diffuse) && ts.emitted.equals(emitted) && ts.specular.equals(specular) && shininess == shininess;
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/FixedFunctionRenderTask.java

+package com.ferox.scene.controller.ffp2;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import com.ferox.math.ColorRGB;
+import com.ferox.math.Vector4;
+import com.ferox.math.bounds.Frustum;
+import com.ferox.renderer.Context;
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.Framework;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.renderer.RenderCapabilities;
+import com.ferox.renderer.Renderer.DrawStyle;
+import com.ferox.renderer.Surface;
+import com.ferox.resource.Texture;
+import com.ferox.resource.VertexAttribute;
+import com.ferox.scene.AmbientLight;
+import com.ferox.scene.AtmosphericFog;
+import com.ferox.scene.BlinnPhongMaterial;
+import com.ferox.scene.Camera;
+import com.ferox.scene.DecalColorMap;
+import com.ferox.scene.DiffuseColor;
+import com.ferox.scene.DiffuseColorMap;
+import com.ferox.scene.DirectionLight;
+import com.ferox.scene.EmittedColor;
+import com.ferox.scene.EmittedColorMap;
+import com.ferox.scene.Light;
+import com.ferox.scene.PointLight;
+import com.ferox.scene.Renderable;
+import com.ferox.scene.SpecularColor;
+import com.ferox.scene.SpecularColorMap;
+import com.ferox.scene.SpotLight;
+import com.ferox.scene.Transform;
+import com.ferox.scene.Transparent;
+import com.ferox.scene.controller.PVSResult;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+import com.ferox.scene.controller.light.LightGroupResult;
+import com.ferox.util.Bag;
+import com.ferox.util.profile.Profiler;
+import com.lhkbob.entreri.ComponentData;
+import com.lhkbob.entreri.Entity;
+import com.lhkbob.entreri.EntitySystem;
+import com.lhkbob.entreri.property.IntProperty;
+import com.lhkbob.entreri.property.ObjectProperty;
+import com.lhkbob.entreri.task.Job;
+import com.lhkbob.entreri.task.ParallelAware;
+import com.lhkbob.entreri.task.Task;
+
+public class FixedFunctionRenderTask implements Task, ParallelAware {
+    private static final Set<Class<? extends ComponentData<?>>> COMPONENTS;
+    static {
+        Set<Class<? extends ComponentData<?>>> types = new HashSet<Class<? extends ComponentData<?>>>();
+        types.add(AmbientLight.class);
+        types.add(DirectionLight.class);
+        types.add(SpotLight.class);
+        types.add(PointLight.class);
+        types.add(Renderable.class);
+        types.add(Transform.class);
+        types.add(BlinnPhongMaterial.class);
+        types.add(DiffuseColor.class);
+        types.add(EmittedColor.class);
+        types.add(SpecularColor.class);
+        types.add(DiffuseColorMap.class);
+        types.add(EmittedColorMap.class);
+        types.add(SpecularColorMap.class);
+        types.add(DecalColorMap.class);
+        types.add(Transparent.class);
+        types.add(Camera.class);
+        types.add(AtmosphericFog.class);
+        COMPONENTS = Collections.unmodifiableSet(types);
+    }
+
+    private final Framework framework;
+    private final boolean flush;
+
+    // alternating frame storage so one frame can be prepared while the
+    // other is being rendering
+    private final Frame frameA;
+    private final Frame frameB;
+
+    private final int shadowmapTextureUnit;
+
+    // per-frame data
+    private List<PVSResult> cameraPVS;
+    private List<PVSResult> lightPVS;
+    private LightGroupResult lightGroups;
+
+    private Frame inuseFrame;
+    private final Queue<Future<Void>> previousFrame;
+
+    // cached local instances
+    private Camera camera;
+    private Transform transform;
+
+    private AmbientLight ambientLight;
+    private DirectionLight directionLight;
+    private SpotLight spotLight;
+    private PointLight pointLight;
+
+    private Renderable renderable;
+    private BlinnPhongMaterial blinnPhong;
+    private DiffuseColor diffuseColor;
+    private SpecularColor specularColor;
+    private EmittedColor emittedColor;
+    private DiffuseColorMap diffuseTexture;
+    private DecalColorMap decalTexture;
+    private EmittedColorMap emittedTexture;
+    private Transparent transparent; // FIXME not fully implemented either
+
+    // FIXME not implemented yet
+    //    private AtmosphericFog fog;
+
+    public FixedFunctionRenderTask(Framework framework) {
+        this(framework, 1024, true);
+    }
+
+    public FixedFunctionRenderTask(Framework framework, int shadowMapSize, boolean flush) {
+        if (framework == null) {
+            throw new NullPointerException("Framework cannot be null");
+        }
+
+        RenderCapabilities caps = framework.getCapabilities();
+        if (!caps.hasFixedFunctionRenderer()) {
+            throw new IllegalArgumentException("Framework must support a FixedFunctionRenderer");
+        }
+
+        this.framework = framework;
+        this.flush = flush;
+
+        previousFrame = new ArrayDeque<Future<Void>>();
+
+        ShadowMapCache shadowMapA;
+        ShadowMapCache shadowMapB;
+
+        int numTex = caps.getMaxFixedPipelineTextures();
+        boolean shadowsRequested = shadowMapSize > 0; // size is positive
+        boolean shadowSupport = ((caps.getFboSupport() || caps.getPbufferSupport()) && numTex > 1 && caps.getDepthTextureSupport());
+
+        if (shadowsRequested && shadowSupport) {
+            // convert size to a power of two
+            int sz = 1;
+            while (sz < shadowMapSize) {
+                sz = sz << 1;
+            }
+            shadowMapA = new ShadowMapCache(framework, sz, sz);
+            shadowMapB = new ShadowMapCache(framework, sz, sz);
+
+            // use the 4th unit if available, or the last unit if we're under
+            shadowmapTextureUnit = Math.max(numTex, 3) - 1;
+            // reserve one unit for the shadow map
+            numTex--;
+        } else {
+            shadowMapA = null;
+            shadowMapB = null;
+            shadowmapTextureUnit = -1;
+        }
+
+        int diffuseTextureUnit, emissiveTextureUnit, decalTextureUnit;
+        if (numTex >= 3) {
+            diffuseTextureUnit = 0;
+            emissiveTextureUnit = 2;
+            decalTextureUnit = 1;
+        } else if (numTex == 2) {
+            diffuseTextureUnit = 0;
+            // merge emissive and decal units
+            emissiveTextureUnit = 1;
+            decalTextureUnit = 1;
+        } else if (numTex == 1) {
+            // merge all units
+            diffuseTextureUnit = 0;
+            emissiveTextureUnit = 0;
+            decalTextureUnit = 0;
+        } else {
+            // disable texturing
+            diffuseTextureUnit = -1;
+            emissiveTextureUnit = -1;
+            decalTextureUnit = -1;
+        }
+
+        frameA = new Frame(shadowMapA,
+                           diffuseTextureUnit,
+                           decalTextureUnit,
+                           emissiveTextureUnit);
+        frameB = new Frame(shadowMapB,
+                           diffuseTextureUnit,
+                           decalTextureUnit,
+                           emissiveTextureUnit);
+    }
+
+    @Override
+    public void reset(EntitySystem system) {
+        if (transform == null) {
+            transform = system.createDataInstance(Transform.class);
+            camera = system.createDataInstance(Camera.class);
+
+            ambientLight = system.createDataInstance(AmbientLight.class);
+            directionLight = system.createDataInstance(DirectionLight.class);
+            spotLight = system.createDataInstance(SpotLight.class);
+            pointLight = system.createDataInstance(PointLight.class);
+
+            renderable = system.createDataInstance(Renderable.class);
+            blinnPhong = system.createDataInstance(BlinnPhongMaterial.class);
+            diffuseColor = system.createDataInstance(DiffuseColor.class);
+            specularColor = system.createDataInstance(SpecularColor.class);
+            emittedColor = system.createDataInstance(EmittedColor.class);
+            diffuseTexture = system.createDataInstance(DiffuseColorMap.class);
+            decalTexture = system.createDataInstance(DecalColorMap.class);
+            emittedTexture = system.createDataInstance(EmittedColorMap.class);
+            transparent = system.createDataInstance(Transparent.class);
+
+            frameA.decorate(system);
+            frameB.decorate(system);
+        }
+
+        lightGroups = null;
+        cameraPVS = new ArrayList<PVSResult>();
+        lightPVS = new ArrayList<PVSResult>();
+    }
+
+    public void report(PVSResult pvs) {
+        if (pvs.getSource().getType().equals(Camera.class)) {
+            cameraPVS.add(pvs);
+        } else if (Light.class.isAssignableFrom(pvs.getSource().getType())) {
+            lightPVS.add(pvs);
+        }
+    }
+
+    public void report(LightGroupResult r) {
+        lightGroups = r;
+    }
+
+    @Override
+    public Set<Class<? extends ComponentData<?>>> getAccessedComponents() {
+        return COMPONENTS;
+    }
+
+    @Override
+    public boolean isEntitySetModified() {
+        return false;
+    }
+
+    @Override
+    public Task process(EntitySystem system, Job job) {
+        Profiler.push("render");
+        Frame currentFrame = (inuseFrame == frameA ? frameB : frameA);
+
+        Profiler.push("shadow-map-scene");
+        currentFrame.shadowMap.reset();
+        for (PVSResult light : lightPVS) {
+            currentFrame.shadowMap.cacheShadowScene(light);
+        }
+        Profiler.pop();
+
+        // synchronize render atoms for all visible entities
+        Profiler.push("state-sync");
+        if (Math.random() < .05) {
+            currentFrame.resetStates();
+        }
+
+        RenderAtom atom;
+        for (PVSResult visible : cameraPVS) {
+            for (Entity e : visible.getPotentiallyVisibleSet()) {
+                e.get(renderable); // guaranteed that this one is present
+                atom = currentFrame.atoms.get(renderable.getIndex());
+
+                if (atom == null) {
+                    atom = new RenderAtom();
+                    currentFrame.atoms.set(atom, renderable.getIndex());
+                }
+
+                syncEntityState(e, atom, currentFrame);
+            }
+        }
+        Profiler.pop();
+
+        Profiler.push("build-state-tree");
+        // all visible atoms have valid states now, so we build a 
+        // postman sorted state node tree used for rendering
+        List<com.ferox.renderer.Task<Void>> thisFrame = new ArrayList<com.ferox.renderer.Task<Void>>();
+        for (PVSResult visible : cameraPVS) {
+            visible.getSource().getEntity().get(camera);
+            thisFrame.add(buildTree(visible.getPotentiallyVisibleSet(),
+                                    visible.getFrustum(), currentFrame,
+                                    camera.getSurface()));
+        }
+        Profiler.pop();
+
+        // block until the previous frame has completed, so we no its data
+        // structures are no longer in use and we can swap which frame is active
+        Profiler.push("block-opengl");
+        while (!previousFrame.isEmpty()) {
+            Future<Void> f = previousFrame.poll();
+            try {
+                f.get();
+            } catch (Exception e) {
+                throw new RuntimeException("Previous frame failed", e);
+            }
+        }
+        Profiler.pop();
+
+        // activate frame and queue all tasks
+        inuseFrame = currentFrame;
+        for (com.ferox.renderer.Task<Void> rf : thisFrame) {
+            previousFrame.add(framework.queue(rf));
+        }
+
+        Profiler.pop();
+        return null;
+    }
+
+    private com.ferox.renderer.Task<Void> buildTree(Bag<Entity> pvs, Frustum camera,
+                                                    Frame frame, final Surface surface) {
+        // static tree construction that doesn't depend on entities
+        final StateNode root = new StateNode(new CameraState(camera)); // children = lit, unlit
+        StateNode litNode = new StateNode(frame.litState); // child = shadowmap state
+        StateNode unlitNode = new StateNode(frame.unlitState); // child = texture states
+        StateNode smNode = new StateNode(new ShadowMapState(frame.shadowMap,
+                                                            shadowmapTextureUnit)); // children = light groups
+
+        root.setChild(0, litNode);
+        root.setChild(1, unlitNode);
+        litNode.setChild(0, smNode);
+
+        // insert light group nodes
+        for (int i = 0; i < lightGroups.getGroupCount(); i++) {
+            smNode.setChild(i,
+                            new StateNode(new LightGroupState(lightGroups.getGroup(i),
+                                                              frame.shadowMap.getShadowCastingLights(),
+                                                              framework.getCapabilities()
+                                                                       .getMaxActiveLights(),
+                                                              directionLight,
+                                                              spotLight,
+                                                              pointLight,
+                                                              ambientLight,
+                                                              transform),
+                                          frame.textureState.length));
+        }
+
+        IntProperty groupAssgn = lightGroups.getAssignmentProperty();
+
+        RenderAtom atom;
+        for (Entity e : pvs) {
+            e.get(renderable);
+            atom = frame.atoms.get(renderable.getIndex());
+
+            StateNode firstNode;
+            try {
+                firstNode = (atom.lit ? smNode.getChild(groupAssgn.get(renderable.getIndex())) : unlitNode);
+            } catch (NullPointerException e1) {
+                System.out.println(atom + " " + groupAssgn);
+                throw e1;
+            }
+
+            // texture state
+            StateNode texNode = firstNode.getChild(atom.textureStateIndex);
+            if (texNode == null) {
+                texNode = new StateNode(frame.textureState[atom.textureStateIndex],
+                                        frame.geometryState.length);
+                firstNode.setChild(atom.textureStateIndex, texNode);
+            }
+
+            // geometry state
+            StateNode geomNode = texNode.getChild(atom.geometryStateIndex);
+            if (geomNode == null) {
+                geomNode = new StateNode(frame.geometryState[atom.geometryStateIndex],
+                                         frame.colorState.length);
+                texNode.setChild(atom.geometryStateIndex, geomNode);
+            }
+
+            // color state
+            StateNode colorNode = geomNode.getChild(atom.colorStateIndex);
+            if (colorNode == null) {
+                colorNode = new StateNode(frame.colorState[atom.colorStateIndex],
+                                          frame.renderState.length);
+                geomNode.setChild(atom.colorStateIndex, colorNode);
+            }
+
+            // render state
+            StateNode renderNode = colorNode.getChild(atom.renderStateIndex);
+            if (renderNode == null) {
+                // must clone the geometry since each node accumulates its own
+                // packed transforms that must be rendered
+                renderNode = new StateNode(frame.renderState[atom.renderStateIndex].cloneGeometry());
+                colorNode.setChild(atom.renderStateIndex, renderNode);
+            }
+
+            // now record the transform into the render node's state
+            e.get(transform);
+            ((RenderState) renderNode.getState()).add(transform.getMatrix());
+        }
+
+        // every entity in the PVS has been put into the tree, which is automatically
+        // clustered by the defined state hierarchy
+        return new com.ferox.renderer.Task<Void>() {
+            @Override
+            public Void run(HardwareAccessLayer access) {
+                Context ctx = access.setActiveSurface(surface);
+                if (ctx != null) {
+                    FixedFunctionRenderer ffp = ctx.getFixedFunctionRenderer();
+                    // FIXME clear color should be configurable somehow
+                    ffp.clear(true, true, true, new Vector4(0, 0, 0, 1.0), 1, 0);
+                    ffp.setDrawStyle(DrawStyle.SOLID, DrawStyle.SOLID);
+                    root.visit(new AppliedEffects(), access);
+
+                    if (flush) {
+                        ctx.flush();
+                    }
+                }
+                return null;
+            }
+        };
+    }
+
+    // FIXME this is the bottleneck now
+    // I need to figure out if it's just expensive because of accessing all
+    // of the components, or because of the many, many if statements, or
+    // if there is a bug and it keeps rebuilding it more than expected
+    private void syncEntityState(Entity e, RenderAtom atom, Frame frame) {
+        // sync render state
+        e.get(renderable);
+        boolean renderableChanged = atom.renderableVersion != renderable.getVersion();
+        if (renderableChanged || atom.renderStateIndex < 0) {
+            atom.renderableVersion = renderable.getVersion();
+            atom.renderStateIndex = frame.getRenderState(renderable);
+        }
+
+        // sync geometry state
+        e.get(blinnPhong);
+        boolean blinnChanged = (blinnPhong.isEnabled() ? atom.blinnPhongVersion != blinnPhong.getVersion() : atom.blinnPhongVersion >= 0);
+        if (renderableChanged || blinnChanged || atom.geometryStateIndex < 0) {
+            // renderable version already synced above
+            atom.blinnPhongVersion = blinnPhong.getVersion();
+            atom.geometryStateIndex = frame.getGeometryState(renderable, blinnPhong);
+        }
+
+        // sync texture state
+        e.get(diffuseTexture);
+        e.get(decalTexture);
+        e.get(emittedTexture);
+        boolean dftChanged = (diffuseTexture.isEnabled() ? atom.diffuseTextureVersion != diffuseTexture.getVersion() : atom.diffuseTextureVersion >= 0);
+        boolean dctChanged = (decalTexture.isEnabled() ? atom.decalTextureVersion != decalTexture.getVersion() : atom.decalTextureVersion >= 0);
+        boolean emtChanged = (emittedTexture.isEnabled() ? atom.emittedTextureVersion != emittedTexture.getVersion() : atom.emittedTextureVersion >= 0);
+        if (dftChanged || dctChanged || emtChanged || atom.textureStateIndex < 0) {
+            atom.diffuseTextureVersion = diffuseTexture.getVersion();
+            atom.decalTextureVersion = decalTexture.getVersion();
+            atom.emittedTextureVersion = emittedTexture.getVersion();
+            atom.textureStateIndex = frame.getTextureState(diffuseTexture, decalTexture,
+                                                           emittedTexture);
+        }
+
+        // sync color state
+        e.get(diffuseColor);
+        e.get(specularColor);
+        e.get(emittedColor);
+        e.get(transparent);
+        boolean dfcChanged = (diffuseColor.isEnabled() ? atom.diffuseColorVersion != diffuseColor.getVersion() : atom.diffuseColorVersion >= 0);
+        boolean spcChanged = (specularColor.isEnabled() ? atom.specularColorVersion != specularColor.getVersion() : atom.specularColorVersion >= 0);
+        boolean emcChanged = (emittedColor.isEnabled() ? atom.emittedColorVersion != emittedColor.getVersion() : atom.emittedColorVersion >= 0);
+        boolean transparentChanged = (transparent.isEnabled() ? atom.transparentVersion != transparent.getVersion() : atom.transparentVersion >= 0);
+        if (dfcChanged || spcChanged || emcChanged || blinnChanged || transparentChanged || atom.colorStateIndex < 0) {
+            // blinn phong version already synced
+            atom.diffuseColorVersion = diffuseColor.getVersion();
+            atom.specularColorVersion = specularColor.getVersion();
+            atom.emittedColorVersion = emittedColor.getVersion();
+            atom.colorStateIndex = frame.getColorState(diffuseColor, specularColor,
+                                                       emittedColor, transparent,
+                                                       blinnPhong);
+        }
+
+        // lit state
+        atom.lit = blinnPhong.isEnabled();
+    }
+
+    private static class Frame {
+        final ShadowMapCache shadowMap;
+
+        //FIXME        TransparentState[] transparentStates;
+        final LightingState litState;
+        final LightingState unlitState;
+
+        final int diffuseTextureUnit;
+        final int emissiveTextureUnit;
+        final int decalTextureUnit;
+
+        TextureState[] textureState;
+        GeometryState[] geometryState;
+        ColorState[] colorState;
+        RenderState[] renderState;
+
+        Map<TextureState, Integer> textureLookup;
+        Map<GeometryState, Integer> geometryLookup;
+        Map<ColorState, Integer> colorLookup;
+        Map<RenderState, Integer> renderLookup;
+
+        // per-entity tracking
+        ObjectProperty<RenderAtom> atoms;
+
+        Frame(ShadowMapCache map, int diffuseTextureUnit, int decalTextureUnit,
+              int emissiveTextureUnit) {
+            shadowMap = map;
+
+            this.diffuseTextureUnit = diffuseTextureUnit;
+            this.decalTextureUnit = decalTextureUnit;
+            this.emissiveTextureUnit = emissiveTextureUnit;
+
+            litState = new LightingState(true);
+            unlitState = new LightingState(false);
+
+            textureState = new TextureState[0];
+            geometryState = new GeometryState[0];
+            colorState = new ColorState[0];
+            renderState = new RenderState[0];
+
+            textureLookup = new HashMap<TextureState, Integer>();
+            geometryLookup = new HashMap<GeometryState, Integer>();
+            colorLookup = new HashMap<ColorState, Integer>();
+            renderLookup = new HashMap<RenderState, Integer>();
+        }
+
+        int getTextureState(DiffuseColorMap diffuse, DecalColorMap decal,
+                            EmittedColorMap emitted) {
+            Texture diffuseTex = (diffuse.isEnabled() ? diffuse.getTexture() : null);
+            VertexAttribute diffuseCoord = (diffuse.isEnabled() ? diffuse.getTextureCoordinates() : null);
+            Texture decalTex = (decal.isEnabled() ? decal.getTexture() : null);
+            VertexAttribute decalCoord = (decal.isEnabled() ? decal.getTextureCoordinates() : null);
+            Texture emittedTex = (emitted.isEnabled() ? emitted.getTexture() : null);
+            VertexAttribute emittedCoord = (emitted.isEnabled() ? emitted.getTextureCoordinates() : null);
+
+            TextureState state = new TextureState(diffuseTextureUnit,
+                                                  decalTextureUnit,
+                                                  emissiveTextureUnit);
+            state.set(diffuseTex, diffuseCoord, decalTex, decalCoord, emittedTex,
+                      emittedCoord);
+
+            Integer index = textureLookup.get(state);
+            if (index == null) {
+                // must create a new state
+                index = textureState.length;
+                textureState = Arrays.copyOf(textureState, textureState.length + 1);
+                textureState[index] = state;
+                textureLookup.put(state, index);
+            }
+
+            return index;
+        }
+
+        int getGeometryState(Renderable renderable, BlinnPhongMaterial blinnPhong) {
+            VertexAttribute verts = renderable.getVertices();
+            VertexAttribute norms = (blinnPhong.isEnabled() ? blinnPhong.getNormals() : null);
+
+            GeometryState state = new GeometryState();
+            state.set(verts, norms);
+
+            Integer index = geometryLookup.get(state);
+            if (index == null) {
+                // needs a new state
+                index = geometryState.length;
+                geometryState = Arrays.copyOf(geometryState, geometryState.length + 1);
+                geometryState[index] = state;
+                geometryLookup.put(state, index);
+            }
+
+            return index;
+        }
+
+        int getColorState(DiffuseColor diffuse, SpecularColor specular,
+                          EmittedColor emitted, Transparent transparent,
+                          BlinnPhongMaterial blinnPhong) {
+            double alpha = (transparent.isEnabled() ? transparent.getOpacity() : 1.0);
+            double shininess = (blinnPhong.isEnabled() ? blinnPhong.getShininess() : 0.0);
+            ColorRGB d = (diffuse.isEnabled() ? diffuse.getColor() : null);
+            ColorRGB s = (specular.isEnabled() ? specular.getColor() : null);
+            ColorRGB e = (emitted.isEnabled() ? emitted.getColor() : null);
+
+            ColorState state = new ColorState();
+            state.set(d, s, e, alpha, shininess);
+
+            Integer index = colorLookup.get(state);
+            if (index == null) {
+                // must form a new state
+                index = colorState.length;
+                colorState = Arrays.copyOf(colorState, colorState.length + 1);
+                colorState[index] = state;
+                colorLookup.put(state, index);
+            }
+
+            return index;
+        }
+
+        int getRenderState(Renderable renderable) {
+            // we can assume that the renderable is always valid, since
+            // we're processing renderable entities
+            RenderState state = new RenderState();
+            state.set(renderable.getPolygonType(), renderable.getIndices(),
+                      renderable.getIndexOffset(), renderable.getIndexCount());
+
+            Integer index = renderLookup.get(state);
+            if (index == null) {
+                // must form a new state
+                index = renderState.length;
+                renderState = Arrays.copyOf(renderState, renderState.length + 1);
+                renderState[index] = state;
+                renderLookup.put(state, index);
+            }
+
+            return index;
+        }
+
+        void resetStates() {
+            textureState = new TextureState[0];
+            geometryState = new GeometryState[0];
+            colorState = new ColorState[0];
+            renderState = new RenderState[0];
+
+            textureLookup = new HashMap<TextureState, Integer>();
+            geometryLookup = new HashMap<GeometryState, Integer>();
+            colorLookup = new HashMap<ColorState, Integer>();
+            renderLookup = new HashMap<RenderState, Integer>();
+
+            // clearing the render atoms effectively invalidates all of the
+            // version tracking we do as well
+            Arrays.fill(atoms.getIndexedData(), null);
+        }
+
+        @SuppressWarnings("unchecked")
+        void decorate(EntitySystem system) {
+            atoms = system.decorate(Renderable.class, new ObjectProperty.Factory(null));
+        }
+    }
+
+    private static class RenderAtom {
+        // state indices
+        int textureStateIndex = -1; // depends on the 3 texture versions
+        int colorStateIndex = -1; // depends on blinnphong-material and 3 color versions
+        int geometryStateIndex = -1; // depends on renderable, blinnphong-material
+        int renderStateIndex = -1; // depends on indices within renderable
+        boolean lit = false;
+
+        // component versions
+        int renderableVersion = -1;
+        int diffuseColorVersion = -1;
+        int emittedColorVersion = -1;
+        int specularColorVersion = -1;
+        int diffuseTextureVersion = -1;
+        int emittedTextureVersion = -1;
+        int decalTextureVersion = -1;
+        int blinnPhongVersion = -1;
+        int transparentVersion = -1;
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/GeometryState.java

+package com.ferox.scene.controller.ffp2;
+
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.resource.VertexAttribute;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+
+public class GeometryState implements State {
+    private VertexAttribute vertices;
+    private VertexAttribute normals;
+
+    public void set(VertexAttribute vertices, VertexAttribute normals) {
+        this.vertices = vertices;
+        this.normals = normals;
+    }
+
+    public VertexAttribute getVertices() {
+        return vertices;
+    }
+
+    public VertexAttribute getNormals() {
+        return normals;
+    }
+
+    @Override
+    public void visitNode(StateNode currentNode, AppliedEffects effects,
+                          HardwareAccessLayer access) {
+        FixedFunctionRenderer r = access.getCurrentContext().getFixedFunctionRenderer();
+
+        r.setVertices(vertices);
+        r.setNormals(normals);
+
+        currentNode.visitChildren(effects, access);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 17;
+        hash = 31 * hash + (vertices == null ? 0 : vertices.hashCode());
+        hash = 31 * hash + (normals == null ? 0 : normals.hashCode());
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof GeometryState)) {
+            return false;
+        }
+
+        GeometryState ts = (GeometryState) o;
+        return nullEquals(ts.normals, normals) && nullEquals(ts.vertices, vertices);
+    }
+
+    private static boolean nullEquals(Object a, Object b) {
+        return (a == null ? b == null : a.equals(b));
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/LightGroupState.java

+package com.ferox.scene.controller.ffp2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.ferox.math.ColorRGB;
+import com.ferox.math.Const;
+import com.ferox.math.Matrix4;
+import com.ferox.math.Vector3;
+import com.ferox.math.Vector4;
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.renderer.Renderer.BlendFactor;
+import com.ferox.scene.AmbientLight;
+import com.ferox.scene.DirectionLight;
+import com.ferox.scene.Light;
+import com.ferox.scene.PointLight;
+import com.ferox.scene.SpotLight;
+import com.ferox.scene.Transform;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+import com.lhkbob.entreri.Component;
+import com.lhkbob.entreri.Entity;
+
+public class LightGroupState implements State {
+    private static final Vector4 BLACK = new Vector4(0.0, 0.0, 0.0, 1.0);
+
+    private final Set<Component<? extends Light<?>>> shadowCastingLights;
+    private final LightBatch[] batches;
+
+    public LightGroupState(Set<Component<? extends Light<?>>> lightGroup,
+                           Set<Component<? extends Light<?>>> shadowCastingLights,
+                           int maxLights, DirectionLight dl, SpotLight sl, PointLight pl,
+                           AmbientLight al, Transform t) {
+        this.shadowCastingLights = shadowCastingLights;
+
+        List<LightBatch> states = new ArrayList<LightBatch>();
+        List<Component<?>> unassignedAmbientLights = new ArrayList<Component<?>>();
+
+        LightBatch currentState = new LightBatch(maxLights); // always have one state, even if empty
+        states.add(currentState);
+
+        int index = 0;
+        for (Component<? extends Light<?>> light : lightGroup) {
+            Entity e = light.getEntity();
+            GLLight gl = null;
+
+            if (light.getType().equals(DirectionLight.class) && e.get(dl)) {
+                gl = new GLLight();
+                e.get(t);
+                gl.setDirectionLight(light, dl.getColor(), t.getMatrix());
+            } else if (light.getType().equals(SpotLight.class) && e.get(sl)) {
+                gl = new GLLight();
+                e.get(t);
+                gl.setSpotLight(light, sl.getColor(), sl.getCutoffAngle(),
+                                sl.getFalloffDistance(), t.getMatrix());
+            } else if (light.getType().equals(PointLight.class) && e.get(pl)) {
+                gl = new GLLight();
+                e.get(t);
+                gl.setPointLight(light, pl.getColor(), pl.getFalloffDistance(),
+                                 t.getMatrix());
+            } else if (light.getType().equals(AmbientLight.class)) {
+                if (currentState.ambientColor == null) {
+                    // merge ambient light with this state group
+                    e.get(al);
+                    currentState.setAmbientLight(al.getColor());
+                } else {
+                    // store the ambient light for later
+                    unassignedAmbientLights.add(light);
+                }
+            }
+
+            if (gl != null) {
+                // have a light to store in the current state
+                if (index >= maxLights) {
+                    // must move on to a new state
+                    currentState = new LightBatch(maxLights);
+                    states.add(currentState);
+                    index = 0;
+                }
+                currentState.setLight(index++, gl);
+            }
+        }
+
+        // process any ambient lights that need to go into a state group
+        if (!unassignedAmbientLights.isEmpty()) {
+            for (int j = 0; j < states.size() && !unassignedAmbientLights.isEmpty(); j++) {
+                if (states.get(j).ambientColor == null) {
+                    // this state can take an ambient light color
+                    Component<?> light = unassignedAmbientLights.remove(unassignedAmbientLights.size() - 1);
+                    if (light.getEntity().get(al)) {
+                        states.get(j).setAmbientLight(al.getColor());
+                    }
+                }
+            }
+
+            // if there are still ambient lights, we need one state for
+            // each ambient light without any other configuration
+            while (!unassignedAmbientLights.isEmpty()) {
+                Component<?> light = unassignedAmbientLights.remove(unassignedAmbientLights.size() - 1);
+                if (light.getEntity().get(al)) {
+                    LightBatch state = new LightBatch(0);
+                    state.setAmbientLight(al.getColor());
+                    states.add(state);
+                }
+            }
+        }
+
+        batches = states.toArray(new LightBatch[states.size()]);
+    }
+
+    @Override
+    public void visitNode(StateNode currentNode, AppliedEffects effects,
+                          HardwareAccessLayer access) {
+        // we only do blending overriding when accumulating into the previous
+        // lights that were already rendered. If we're in the shadowing pass, we
+        // know that only a single light is active so it doesn't matter if the index > 0,
+        // we don't need to touch the blending
+        boolean needsBlending = batches.length > 1 && !effects.isShadowBeingRendered();
+        FixedFunctionRenderer r = access.getCurrentContext().getFixedFunctionRenderer();
+
+        // render first batch with regular blending
+        AppliedEffects batchEffects = effects;
+        batches[0].renderBatch(0, currentNode, batchEffects, access);
+
+        if (needsBlending) {
+            // update blending state
+            batchEffects = effects.applyBlending(effects.getSourceBlendFactor(),
+                                                 BlendFactor.ONE);
+            batchEffects.pushBlending(r);
+        }
+
+        // render all other batches
+        for (int i = 1; i < batches.length; i++) {
+            batches[i].renderBatch(i, currentNode, batchEffects, access);
+        }
+
+        if (needsBlending) {
+            // overwrote blending state, so restore it
+            effects.pushBlending(r);
+        }
+    }
+
+    private class LightBatch {
+        private Vector4 ambientColor;
+
+        private final GLLight[] lights;
+
+        public LightBatch(int maxLights) {
+            ambientColor = null;
+            lights = new GLLight[maxLights];
+        }
+
+        public void setAmbientLight(@Const ColorRGB color) {
+            ambientColor = convertColor(color);
+        }
+
+        public void setLight(int light, GLLight glLight) {
+            lights[light] = glLight;
+        }
+
+        public void renderBatch(int batch, StateNode currentNode, AppliedEffects effects,
+                                HardwareAccessLayer access) {
+            FixedFunctionRenderer r = access.getCurrentContext()
+                                            .getFixedFunctionRenderer();
+            int numLights = 0;
+
+            if (!effects.isShadowBeingRendered() && ambientColor != null) {
+                // use the defined ambient color during the main render stage,
+                // but not when we're being accumulated for the shadow stage
+                r.setGlobalAmbientLight(ambientColor);
+                numLights++;
+            } else {
+                // default ambient color is black, and if we're in an SM pass,
+                // we don't want to apply ambient light multiple times
+                r.setGlobalAmbientLight(BLACK);
+            }
+
+            GLLight light;
+            for (int i = 0; i < lights.length; i++) {
+                light = lights[i];
+
+                if (light != null) {
+                    // check to see if this light should be used for the current
+                    // stage of shadow mapping
+                    boolean ifInSM = effects.isShadowBeingRendered() && light.source == effects.getShadowMappingLight();
+                    boolean notInSM = !effects.isShadowBeingRendered() && !shadowCastingLights.contains(light.source);
+
+                    if (ifInSM || notInSM) {
+                        // enable and configure the light
+                        r.setLightEnabled(i, true);
+                        r.setLightPosition(i, light.position);
+                        r.setLightColor(i, BLACK, light.color, light.color);
+
+                        if (light.spotlightDirection != null) {
+                            // configure additional spotlight parameters
+                            r.setSpotlight(i, light.spotlightDirection, light.cutoffAngle);
+                            if (light.falloff >= 0) {
+                                // the constant 15 was chosen through experimentation, basically
+                                // a value that makes lights seem bright enough but still
+                                // drop off pretty well by the desired radius
+                                r.setLightAttenuation(i,
+                                                      1.0,
+                                                      0.0,
+                                                      (15.0 / (light.falloff * light.falloff)));
+                            } else {
+                                // disable attenuation
+                                r.setLightAttenuation(i, 1.0, 0.0, 0.0);
+                            }
+                        }
+
+                        numLights++;
+                    } else {
+                        // disable the light
+                        r.setLightEnabled(i, false);
+                    }
+                } else {
+                    // disable the light
+                    r.setLightEnabled(i, false);
+                }
+            }
+
+            if (numLights > 0) {
+                // render because at least one new light is affecting the scene
+                currentNode.visitChildren(effects, access);
+            } else if (batch == 0 && !effects.isShadowBeingRendered()) {
+                // there are no configured lights, but render the object's
+                // silhouettes to make sure the depth buffer, etc. get filled
+                // as expected
+                currentNode.visitChildren(effects, access);
+            }
+        }
+    }
+
+    private static class GLLight {
+        Vector4 color;
+        Vector4 position;
+        Vector3 spotlightDirection;
+        double cutoffAngle;
+        double falloff;
+
+        Component<? extends Light<?>> source;
+
+        public void setDirectionLight(Component<? extends Light<?>> light,
+                                      ColorRGB color, Matrix4 transform) {
+            position = new Vector4(-transform.m02, -transform.m12, -transform.m22, 0.0);
+            this.color = convertColor(color);
+            spotlightDirection = null;
+            source = light;
+        }
+
+        public void setSpotLight(Component<? extends Light<?>> light, ColorRGB color,
+                                 double cutoffAngle, double falloff, Matrix4 transform) {
+            position = new Vector4(transform.m03, transform.m13, transform.m23, 1.0);
+            this.color = convertColor(color);
+            spotlightDirection = new Vector3(transform.m02, transform.m12, transform.m22);
+            this.cutoffAngle = cutoffAngle;
+            this.falloff = falloff;
+            source = light;
+        }
+
+        public void setPointLight(Component<? extends Light<?>> light, ColorRGB color,
+                                  double falloff, Matrix4 transform) {
+            position = new Vector4(transform.m03, transform.m13, transform.m23, 1.0);
+            this.color = convertColor(color);
+            spotlightDirection = new Vector3(0.0, 0.0, 1.0); // any non-null direction is fine
+            this.cutoffAngle = 180.0;
+            this.falloff = falloff;
+            source = light;
+        }
+    }
+
+    private static Vector4 convertColor(ColorRGB color) {
+        return new Vector4(color.redHDR(), color.greenHDR(), color.blueHDR(), 1.0);
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/LightingState.java

+package com.ferox.scene.controller.ffp2;
+
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+
+public class LightingState implements State {
+    private final boolean lit;
+
+    public LightingState(boolean lit) {
+        this.lit = lit;
+    }
+
+    public boolean isLightingEnabled() {
+        return lit;
+    }
+
+    @Override
+    public void visitNode(StateNode currentNode, AppliedEffects effects,
+                          HardwareAccessLayer access) {
+        FixedFunctionRenderer r = access.getCurrentContext().getFixedFunctionRenderer();
+        r.setLightingEnabled(lit);
+
+        currentNode.visitChildren(effects, access);
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/RenderState.java

+package com.ferox.scene.controller.ffp2;
+
+import java.util.Arrays;
+
+import com.ferox.math.Const;
+import com.ferox.math.Matrix4;
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.renderer.Renderer.PolygonType;
+import com.ferox.resource.VertexBufferObject;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+
+public class RenderState implements State {
+    private VertexBufferObject indices;
+    private int indexOffset;
+    private int indexCount;
+    private PolygonType polyType;
+
+    private final Matrix4 modelMatrix = new Matrix4();
+
+    // packed objects to render
+    private float[] matrices;
+    private int count;
+
+    public RenderState() {
+        matrices = new float[16];
+        count = 0;
+    }
+
+    public void set(PolygonType polyType, VertexBufferObject indices, int offset,
+                    int count) {
+        this.polyType = polyType;
+        this.indices = indices;
+        indexOffset = offset;
+        indexCount = count;
+    }
+
+    public PolygonType getPolygonType() {
+        return polyType;
+    }
+
+    public VertexBufferObject getIndices() {
+        return indices;
+    }
+
+    public int getIndexCount() {
+        return indexCount;
+    }
+
+    public int getIndexOffset() {
+        return indexOffset;
+    }
+
+    public void add(@Const Matrix4 transform) {
+        if (count + 16 > matrices.length) {
+            // grow array
+            matrices = Arrays.copyOf(matrices, matrices.length * 2);
+        }
+
+        // use provided matrix
+        transform.get(matrices, count, false);
+
+        count += 16;
+    }
+
+    public void clear() {
+        count = 0;
+    }
+
+    public RenderState cloneGeometry() {
+        RenderState r = new RenderState();
+        r.set(polyType, indices, indexOffset, indexCount);
+        return r;
+    }
+
+    @Override
+    public void visitNode(StateNode currentNode, AppliedEffects effects,
+                          HardwareAccessLayer access) {
+        FixedFunctionRenderer r = access.getCurrentContext().getFixedFunctionRenderer();
+
+        if (indices == null) {
+            for (int i = 0; i < count; i += 16) {
+                // load and multiply the model with the view
+                modelMatrix.set(matrices, i, false);
+                modelMatrix.mul(effects.getViewMatrix(), modelMatrix);
+
+                r.setModelViewMatrix(modelMatrix);
+                r.render(polyType, indexOffset, indexCount);
+            }
+        } else {
+            for (int i = 0; i < count; i += 16) {
+                // load and multiply the model with the view
+                modelMatrix.set(matrices, i, false);
+                modelMatrix.mul(effects.getViewMatrix(), modelMatrix);
+
+                r.setModelViewMatrix(modelMatrix);
+                r.render(polyType, indices, indexOffset, indexCount);
+            }
+        }
+
+        // restore modelview matrix for lighting, etc.
+        r.setModelViewMatrix(effects.getViewMatrix());
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 17;
+        hash = 31 * hash + (indices == null ? 0 : indices.hashCode());
+        hash = 31 * hash + indexOffset;
+        hash = 31 * hash + indexCount;
+        hash = 31 * hash + polyType.hashCode();
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof RenderState)) {
+            return false;
+        }
+        RenderState r = (RenderState) o;
+        return nullEquals(r.indices, indices) && r.indexCount == indexCount && r.indexOffset == indexOffset && r.polyType == polyType;
+    }
+
+    private static boolean nullEquals(Object a, Object b) {
+        return (a == null ? b == null : a.equals(b));
+    }
+}

File ferox-scene/src/main/java/com/ferox/scene/controller/ffp2/ShadowMapCache.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.ffp2;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.ferox.math.Vector4;
+import com.ferox.math.bounds.Frustum;
+import com.ferox.renderer.ContextState;
+import com.ferox.renderer.FixedFunctionRenderer;
+import com.ferox.renderer.Framework;
+import com.ferox.renderer.HardwareAccessLayer;
+import com.ferox.renderer.Renderer.Comparison;
+import com.ferox.renderer.Renderer.DrawStyle;
+import com.ferox.renderer.Surface;
+import com.ferox.renderer.TextureSurface;
+import com.ferox.renderer.TextureSurfaceOptions;
+import com.ferox.resource.Texture;
+import com.ferox.resource.Texture.Filter;
+import com.ferox.resource.Texture.Target;
+import com.ferox.resource.Texture.WrapMode;
+import com.ferox.scene.Light;
+import com.ferox.scene.Renderable;
+import com.ferox.scene.Transform;
+import com.ferox.scene.controller.PVSResult;
+import com.ferox.scene.controller.ffp.AppliedEffects;
+import com.lhkbob.entreri.Component;
+import com.lhkbob.entreri.Entity;
+import com.lhkbob.entreri.EntitySystem;
+
+public class ShadowMapCache {
+    private final TextureSurface shadowMap;
+
+    private Map<Component<? extends Light<?>>, ShadowMapScene> shadowScenes;
+    private Set<Component<? extends Light<?>>> shadowLights;
+
+    public ShadowMapCache(Framework framework, int width, int height) {
+        shadowMap = framework.createSurface(new TextureSurfaceOptions().setWidth(width)
+                                                                       .setHeight(height)
+                                                                       .setDepth(1)
+                                                                       .setTarget(Target.T_2D)
+                                                                       .setUseDepthTexture(true)
+                                                                       .setColorBufferFormats());
+        shadowScenes = new HashMap<Component<? extends Light<?>>, ShadowMapScene>();
+
+        Texture sm = shadowMap.getDepthBuffer();
+        sm.setFilter(Filter.LINEAR);
+        sm.setWrapMode(WrapMode.CLAMP_TO_BORDER);
+        sm.setBorderColor(new Vector4(1, 1, 1, 1));
+        sm.setDepthCompareEnabled(true);
+        sm.setDepthComparison(Comparison.LEQUAL);
+    }
+
+    public void reset() {
+        shadowScenes = new HashMap<Component<? extends Light<?>>, ShadowMapScene>();
+        shadowLights = Collections.unmodifiableSet(shadowScenes.keySet());
+    }
+
+    public Set<Component<? extends Light<?>>> getShadowCastingLights() {
+        return shadowLights;
+    }
+
+    public Frustum getShadowMapFrustum(Component<? extends Light<?>> light) {
+        ShadowMapScene scene = shadowScenes.get(light);
+        if (scene == null) {
+            throw new IllegalArgumentException("Light was not cached previously");
+        }
+        return scene.frustum;
+    }
+
+    public Texture getShadowMap() {
+        return shadowMap.getDepthBuffer();
+    }
+
+    public Texture getShadowMap(Component<? extends Light<?>> shadowLight,
+                                HardwareAccessLayer access) {
+        ShadowMapScene scene = shadowScenes.get(shadowLight);
+        if (scene == null) {
+            throw new IllegalArgumentException("Light was not cached previously");
+        }
+
+        Surface origSurface = access.getCurrentContext().getSurface();
+        int origLayer = access.getCurrentContext().getSurfaceLayer(); // in case of texture-surface
+        ContextState<FixedFunctionRenderer> origState = access.getCurrentContext()
+                                                              .getFixedFunctionRenderer()
+                                                              .getCurrentState();
+
+        FixedFunctionRenderer r = access.setActiveSurface(shadowMap)
+                                        .getFixedFunctionRenderer();
+
+        r.clear(true, true, true);
+        r.setColorWriteMask(false, false, false, false);
+
+        // set style to be just depth, while drawing only back faces
+        r.setDrawStyle(DrawStyle.NONE, DrawStyle.SOLID);
+
+        // move everything backwards slightly to account for floating errors
+        r.setDepthOffsets(0, 5);
+        r.setDepthOffsetsEnabled(true);
+
+        scene.scene.visit(new AppliedEffects(), access);
+
+        // restore original surface and state
+        if (origSurface instanceof TextureSurface) {
+            access.setActiveSurface((TextureSurface) origSurface, origLayer);
+        } else {
+            access.setActiveSurface(origSurface);
+        }
+
+        // FIXME race condition on shutdown exists if the orig surface is
+        // destroyed while rendering to the shadow map, if the map is on a pbuffer
+        access.getCurrentContext().getFixedFunctionRenderer().setCurrentState(origState);
+
+        return shadowMap.getDepthBuffer();
+    }
+
+    @SuppressWarnings("unchecked")
+    public void cacheShadowScene(PVSResult pvs) {
+        if (!Light.class.isAssignableFrom(pvs.getSource().getType())) {
+            // only take PVS's produced by lights
+            return;
+        }
+
+        EntitySystem system = pvs.getSource().getEntitySystem();
+        Renderable renderable = system.createDataInstance(Renderable.class);
+        Transform transform = system.createDataInstance(Transform.class);
+
+        GeometryState geom = new GeometryState();
+        RenderState render = new RenderState();
+
+        // build up required states and tree simultaneously
+        StateNode root = new StateNode(new CameraState(pvs.getFrustum()));
+
+        List<GeometryState> geomLookup = new ArrayList<GeometryState>();
+        List<RenderState> renderLookup = new ArrayList<RenderState>();
+
+        Map<GeometryState, Integer> geomState = new HashMap<GeometryState, Integer>();
+        Map<RenderState, Integer> renderState = new HashMap<RenderState, Integer>();
+        for (Entity e : pvs.getPotentiallyVisibleSet()) {
+            e.get(renderable);
+
+            geom.set(renderable.getVertices(), null); // don't need normals
+            render.set(renderable.getPolygonType(), renderable.getIndices(),
+                       renderable.getIndexOffset(), renderable.getIndexCount());
+
+            Integer geomStateIndex = geomState.get(geom);
+            if (geomStateIndex == null) {
+                geomStateIndex = geomLookup.size();
+                geomLookup.add(geom);
+                geomState.put(geom, geomStateIndex);
+                // create a new state so we don't mutate value stached in collection
+                geom = new GeometryState();
+            }
+            Integer renderStateIndex = geomState.get(geom);
+            if (renderStateIndex == null) {