Michael Ludwig avatar Michael Ludwig committed ff9cd72

Finish implementation of glsl drivers, and add a normal mapping test.

Comments (0)

Files changed (19)

src/com/ferox/renderer/impl/jogl/EnumUtil.java

 import com.ferox.resource.TextureFormat;
 import com.ferox.resource.BufferData.DataType;
 import com.ferox.resource.BufferedGeometry.PolygonType;
+import com.ferox.resource.GlslUniform.UniformType;
+import com.ferox.resource.GlslVertexAttribute.AttributeType;
 import com.ferox.resource.TextureImage.DepthMode;
 import com.ferox.resource.TextureImage.Filter;
 import com.ferox.resource.TextureImage.TextureTarget;
  *
  */
 public class EnumUtil {
+	/** Return the UniformType enum value associated with the returned GL enum
+	 * for uniform variable type.  Returns null if there's no matching UniformType. */
+	public static UniformType getUniformType(int type) {
+		switch(type) {
+		case GL.GL_FLOAT: return UniformType.FLOAT;
+		case GL.GL_FLOAT_VEC2: return UniformType.FLOAT_VEC2;
+		case GL.GL_FLOAT_VEC3: return UniformType.FLOAT_VEC3;
+		case GL.GL_FLOAT_VEC4: return UniformType.FLOAT_VEC4;
+		
+		case GL.GL_FLOAT_MAT2: return UniformType.FLOAT_MAT2;
+		case GL.GL_FLOAT_MAT3: return UniformType.FLOAT_MAT3;
+		case GL.GL_FLOAT_MAT4: return UniformType.FLOAT_MAT4;
+		
+		case GL.GL_INT: return UniformType.INT;
+		case GL.GL_INT_VEC2: return UniformType.INT_VEC2;
+		case GL.GL_INT_VEC3: return UniformType.INT_VEC3;
+		case GL.GL_INT_VEC4: return UniformType.INT_VEC4;
+		
+		case GL.GL_BOOL: return UniformType.BOOL;
+		case GL.GL_BOOL_VEC2: return UniformType.BOOL_VEC2;
+		case GL.GL_BOOL_VEC3: return UniformType.BOOL_VEC3;
+		case GL.GL_BOOL_VEC4: return UniformType.BOOL_VEC4;
+		
+		case GL.GL_SAMPLER_1D: return UniformType.SAMPLER_1D;
+		case GL.GL_SAMPLER_2D: return UniformType.SAMPLER_2D;
+		case GL.GL_SAMPLER_3D: return UniformType.SAMPLER_3D;
+		case GL.GL_SAMPLER_CUBE: return UniformType.SAMPLER_CUBEMAP;
+		case GL.GL_SAMPLER_2D_RECT_ARB: return UniformType.SAMPLER_RECT;
+		case GL.GL_SAMPLER_2D_SHADOW: return UniformType.SAMPLER_2D_SHADOW;
+		case GL.GL_SAMPLER_1D_SHADOW: return UniformType.SAMPLER_1D_SHADOW;
+		case GL.GL_SAMPLER_2D_RECT_SHADOW_ARB: return UniformType.SAMPLER_RECT_SHADOW;
+		}
+
+		return null;
+	}
+	
+	/** Return the AttributeType enum value associated with the returned GL enum
+	 * for attribute variable type.  Returns null if there's no matching AttributeType. */
+	public static AttributeType getAttributeType(int type) {
+		switch(type) {
+		case GL.GL_FLOAT: return AttributeType.FLOAT;
+		case GL.GL_FLOAT_VEC2: return AttributeType.VEC2F;
+		case GL.GL_FLOAT_VEC3: return AttributeType.VEC3F;
+		case GL.GL_FLOAT_VEC4: return AttributeType.VEC4F;
+		case GL.GL_FLOAT_MAT2: return AttributeType.MAT2F;
+		case GL.GL_FLOAT_MAT3: return AttributeType.MAT3F;
+		case GL.GL_FLOAT_MAT4: return AttributeType.MAT4F;
+		}
+
+		return null;
+	}
+	
 	/** Type can't be null. */
 	public static int getGLPolygonConnectivity(PolygonType type) {
 		switch(type) {

src/com/ferox/renderer/impl/jogl/JoglFullscreenSurface.java

 		float weight;
 		for (int i = 1; i < modes.length; i++) {
 			weight = getWeight(modes[i], desiredBitDepth, width, height);
-			if (weight > bestWeight) {
+
+			if (weight < bestWeight) {
 				bestWeight = weight;
 				best = modes[i];
+				System.out.println(weight + " " + best.getBitDepth() + " " + best.getWidth() + " " + best.getHeight() + " " + best.getRefreshRate());
 			}
 		}
-		
 		return best;
 	}
 	
+	// closer to 0 represents better match
 	private static float getWeight(DisplayMode candidate, int bits, int width, int height) {
 		int w = candidate.getWidth();
 		int h = candidate.getHeight();

src/com/ferox/renderer/impl/jogl/JoglOnscreenSurface.java

 		if (optionsRequest == null)
 			optionsRequest = new DisplayOptions();
 		this.canvas = new GLCanvas(chooseCapabilities(optionsRequest), new DefaultGLCapabilitiesChooser(), factory.getShadowContext(), null);
+		this.canvas.setFocusable(false); // to make sure input doesn't get stolen
 		this.canvas.addGLEventListener(this);
 		
 		this.record = new JoglStateRecord(factory.getRenderer().getCapabilities());

src/com/ferox/renderer/impl/jogl/JoglRenderSurface.java

 	public void displaySurface(JoglRenderSurface next) {
 		if (!this.renderedOnce) {
 			this.init();
-			this.renderedOnce = false;
+			this.renderedOnce = true;
 		}
 		
 		this.preRenderAction();

src/com/ferox/renderer/impl/jogl/drivers/JoglGlslProgramResourceDriver.java

+package com.ferox.renderer.impl.jogl.drivers;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.media.opengl.GL;
+
+import com.ferox.renderer.Renderer;
+import com.ferox.renderer.impl.ResourceData;
+import com.ferox.renderer.impl.ResourceDriver;
+import com.ferox.renderer.impl.ResourceData.Handle;
+import com.ferox.renderer.impl.jogl.EnumUtil;
+import com.ferox.renderer.impl.jogl.JoglSurfaceFactory;
+import com.ferox.resource.GlslProgram;
+import com.ferox.resource.GlslUniform;
+import com.ferox.resource.GlslVertexAttribute;
+import com.ferox.resource.Resource;
+import com.ferox.resource.GlslProgram.GlslProgramDirtyDescriptor;
+import com.ferox.resource.GlslUniform.UniformType;
+import com.ferox.resource.GlslVertexAttribute.AttributeType;
+import com.ferox.resource.Resource.Status;
+
+public class JoglGlslProgramResourceDriver implements ResourceDriver {
+	/* Handle subclass used for GlslPrograms. */
+	private static class GlslProgramHandle implements Handle {
+		private int programId;
+		private int vertexId;
+		private int fragmentId;
+				
+		@Override
+		public int getId() {
+			return this.programId;
+		}
+	}
+	
+	/* GlslUniform substitute. */
+	private static class Uniform {
+		private UniformType type;
+		private String name;
+		private int length;
+		
+		public boolean equals(GlslUniform other) {
+			return other.getType() == this.type && other.getName().equals(this.name) && other.getLength() == this.length;
+		}
+	}
+	
+	/* GlslVertexAttribute substitute. */
+	private static class Attribute {
+		private AttributeType type;
+		private String name;
+		private int bindSlot;
+		
+		public boolean equals(GlslVertexAttribute other) {
+			return other.getType() == this.type && other.getName().equals(this.name) && other.getBindingSlot() == this.bindSlot;
+		}
+	}
+	
+	private JoglSurfaceFactory factory;
+	private boolean glslSupport;
+	private int maxAttributes;
+	
+	public JoglGlslProgramResourceDriver(JoglSurfaceFactory factory) {
+		this.factory = factory;
+		this.glslSupport = factory.getRenderer().getCapabilities().getGlslSupport();
+		this.maxAttributes = factory.getRenderer().getCapabilities().getMaxVertexAttributes();
+	}
+	
+	@Override
+	public void cleanUp(Resource resource, ResourceData data) {
+		GL gl = this.factory.getGL();
+		GlslProgramHandle handle = (GlslProgramHandle) data.getHandle();
+		
+		if (handle != null) {
+			// not null implies glsl support
+			
+			if (handle.vertexId > 0)
+				gl.glDeleteShader(handle.vertexId);
+			if (handle.fragmentId > 0)
+				gl.glDeleteShader(handle.fragmentId);
+			if (handle.programId > 0)
+				gl.glDeleteProgram(handle.programId);
+		}
+	}
+
+	@Override
+	public void update(Resource resource, ResourceData data, boolean fullUpdate) {
+		GL gl = this.factory.getGL();
+		GlslProgramHandle handle = (GlslProgramHandle) data.getHandle();
+		GlslProgram program = (GlslProgram) resource;
+		
+		if (handle == null) {
+			if (!this.glslSupport) {
+				// glsl is not supported, so abort the update
+				data.setStatus(Status.ERROR);
+				data.setStatusMessage("GLSL shader programs aren't supported on this hardware");
+				
+				Renderer renderer = this.factory.getRenderer();
+				for (GlslUniform u: program.getUniforms().values()) {
+					// this will set status to ERROR, too
+					renderer.update(u, fullUpdate);
+				}
+				return; // abort
+			}
+			
+			handle = new GlslProgramHandle();
+			data.setHandle(handle);
+			
+			// we always have a program handle, since GlslProgram doesn't allow FFP shaders
+			handle.programId = gl.glCreateProgram();
+			
+			// mark these as invalid so they will be created if need be
+			handle.fragmentId = -1;
+			handle.vertexId = -1;
+			
+			fullUpdate = true;
+		}
+		
+		GlslProgramDirtyDescriptor dirty = program.getDirtyDescriptor();
+		
+		if (fullUpdate || dirty.getAttributesDirty() || dirty.getGlslCodeDirty()) {
+			// re-bind the attribute locations
+			int slot;
+			for (Entry<String, GlslVertexAttribute> entry: program.getAttributes().entrySet()) {
+				slot = entry.getValue().getBindingSlot();
+				if (slot < this.maxAttributes)
+					gl.glBindAttribLocation(handle.programId, entry.getValue().getBindingSlot(), entry.getKey());
+				// else ... ignore it since it's invalid
+			}
+		}
+		
+		if (fullUpdate || dirty.getGlslCodeDirty()) {
+			// set the glsl code that will be executed for the program
+			String[] fragmentCode = program.getFragmentShader();
+			int fragmentObject = (fragmentCode.length == 0 ? -1 : compileShader(gl, handle.fragmentId, 
+																				fragmentCode, GL.GL_FRAGMENT_SHADER));
+			
+			String[] vertexCode = program.getVertexShader();
+			int vertexObject = (vertexCode.length == 0 ? -1 : compileShader(gl, handle.vertexId, 
+																			vertexCode, GL.GL_VERTEX_SHADER));
+			
+			// attach the shaders
+			handle.fragmentId = setShader(gl, handle.programId, handle.fragmentId, fragmentObject);
+			handle.vertexId = setShader(gl, handle.programId, handle.vertexId, vertexObject);
+			
+			// check compile status
+			boolean fragCompiled = (fragmentObject <= 0 ? true : isCompiled(gl, fragmentObject));
+			boolean vertCompiled = (vertexObject <= 0 ? true : isCompiled(gl, vertexObject));
+			
+			// variables used to set the status and status message
+			String fragMsg = "Fragment shader, Compiled: " + fragCompiled + " Log:\n" + getShaderLog(gl, handle.fragmentId);
+			String vertMsg = "Vertex shader, Compiled: " + vertCompiled + " Log:\n" + getShaderLog(gl, handle.vertexId);
+			
+			String progMsg;
+			Status status;
+			
+			if (vertCompiled && fragCompiled) {
+				// we can link the shader program now
+				gl.glLinkProgram(handle.programId);
+				boolean linked = isLinked(gl, handle.programId);
+				
+				progMsg = "Glsl program, Linked: " + linked + " Log:\n" + getProgramLog(gl, handle.programId);
+				status = (linked ? Status.OK : Status.ERROR);
+			} else {
+				// something failed to compile, set status to ERROR
+				status = Status.ERROR;
+				progMsg = "Unable to link shaders because of compile errors";
+			}
+			
+			data.setStatus(status);
+			data.setStatusMessage(vertMsg.trim() + "\n" + fragMsg.trim() + "\n" + progMsg.trim());
+		}
+		
+		// now we have to configure the uniforms and attributes
+		
+		if ((fullUpdate || dirty.getGlslCodeDirty() || dirty.getUniformsDirty()) && data.getStatus() != Status.ERROR) {
+			// have to update the uniforms
+			detectUniforms(gl, this.factory.getRenderer(), program, handle);
+		}
+		
+		if ((fullUpdate || dirty.getGlslCodeDirty() || dirty.getAttributesDirty()) && data.getStatus() != Status.ERROR) {
+			// have to update the attributes and make sure slots don't overlap
+			detectAttributes(gl, program, handle);
+		}
+		
+		program.clearDirtyDescriptor();
+	}
+	
+	/* Fetch declared shader attributes, unbind attributes that don't exist.  Attach any new ones.
+	 * Update the slot bindings for the given program. */
+	private static void detectAttributes(GL gl, GlslProgram program, GlslProgramHandle handle) {
+		int[] totalUniforms = new int[1];
+		gl.glGetProgramiv(handle.programId, GL.GL_ACTIVE_ATTRIBUTES, totalUniforms, 0);
+		
+		int[] maxNameLength = new int[1];
+		gl.glGetProgramiv(handle.programId, GL.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, maxNameLength, 0);
+		
+		int[] nameLength = new int[1];
+		int[] size = new int[1];
+		int[] type = new int[1];
+		byte[] chars = new byte[maxNameLength[0]];
+		
+		Map<String, Attribute> declaredAttributes = new HashMap<String, Attribute>();
+		for (int i = 0; i < totalUniforms[0]; i++) {
+			// read in the attrs from OpenGL
+			gl.glGetActiveAttrib(handle.programId, i, maxNameLength[0], nameLength, 0, size, 0, type, 0, chars, 0);
+			
+			Attribute a = new Attribute();
+			a.type = EnumUtil.getAttributeType(type[0]);
+			a.name = createString(chars, nameLength[0]);
+			
+			// ignore gl_X variables
+			if (!a.name.startsWith("gl")) {
+				a.bindSlot = gl.glGetAttribLocation(handle.programId, a.name);
+				declaredAttributes.put(a.name, a);
+			}
+		}
+		
+		// now we have to clean-up bad attrs, attach new ones, and update good vars
+		
+		Map<String, GlslVertexAttribute> existingAttrs = new HashMap<String, GlslVertexAttribute>(program.getAttributes());
+		Attribute declared;
+		GlslVertexAttribute existing;
+		for (Entry<String, GlslVertexAttribute> entry: existingAttrs.entrySet()) {
+			existing = entry.getValue();
+			declared = declaredAttributes.get(existing.getName());
+			if (declared == null || !declared.equals(existing)) {
+				// a bad bound attrs
+				program.unbindAttribute(existing.getName());
+			} else {
+				// a good, already attached attr
+				declaredAttributes.remove(existing.getName());
+			}
+		}
+		
+		// at this point, only un-attached valid uniforms are in this map
+		for (Entry<String, Attribute> entry: declaredAttributes.entrySet()) {
+			declared = entry.getValue();
+			// we can use this attribute
+			program.bindAttribute(declared.name, declared.type, declared.bindSlot);
+		}
+	}
+	
+	/* Fetch declared uniforms from the gfx card.  Detach uniforms that no longer exist from the program
+	 * and clean them up.  Attach new uniforms, and update all valid uniforms (including ones attached
+	 * previously). */
+	private static void detectUniforms(GL gl, Renderer renderer, GlslProgram program, GlslProgramHandle handle) {
+		int[] totalUniforms = new int[1];
+		gl.glGetProgramiv(handle.programId, GL.GL_ACTIVE_UNIFORMS, totalUniforms, 0);
+		
+		int[] maxNameLength = new int[1];
+		gl.glGetProgramiv(handle.programId, GL.GL_ACTIVE_UNIFORM_MAX_LENGTH, maxNameLength, 0);
+		
+		int[] nameLength = new int[1];
+		int[] size = new int[1];
+		int[] type = new int[1];
+		byte[] chars = new byte[maxNameLength[0]];
+		
+		Map<String, Uniform> declaredUniforms = new HashMap<String, Uniform>();
+		for (int i = 0; i < totalUniforms[0]; i++) {
+			// read in the uniform from OpenGL
+			gl.glGetActiveUniform(handle.programId, i, maxNameLength[0], nameLength, 0, size, 0, type, 0, chars, 0);
+			
+			Uniform u = new Uniform();
+			u.length = size[0];
+			u.type = EnumUtil.getUniformType(type[0]);
+			u.name = createString(chars, nameLength[0]);
+
+			// ignore gl_X variables
+			if (!u.name.startsWith("gl"))
+				declaredUniforms.put(u.name, u);
+		}
+		
+		// now we have to clean-up bad uniforms, attach new ones, and update good vars
+		Map<String, GlslUniform> existingUniforms = new HashMap<String, GlslUniform>(program.getUniforms());
+		Uniform declared;
+		GlslUniform existing;
+		for (Entry<String, GlslUniform> entry: existingUniforms.entrySet()) {
+			existing = entry.getValue();
+			declared = declaredUniforms.get(existing.getName());
+			if (declared == null || !declared.equals(existing)) {
+				// a bad attached uniform
+				program.detachUniform(existing.getName());
+				renderer.cleanUp(existing);
+			} else {
+				// a good, already attached uniform
+				declaredUniforms.remove(existing.getName());
+				renderer.update(existing, true);
+			}
+		}
+		
+		// at this point, only un-attached valid uniforms are in this map
+		for (Entry<String, Uniform> entry: declaredUniforms.entrySet()) {
+			declared = entry.getValue();
+			existing = program.attachUniform(declared.name, declared.type, declared.length);
+			renderer.update(existing, true);
+		}
+	}
+	
+	/* If oldShader differs from newShader, it will detach and delete oldShader (if 
+	 * oldShader existed), and attach newShader (if newShader exists).  Returns newShader. */
+	private static int setShader(GL gl, int programId, int oldShader, int newShader) {
+		if (oldShader != newShader) {
+			if (oldShader > 0) {
+				// clean-up old shader object
+				gl.glDetachShader(programId, oldShader);
+				gl.glDeleteShader(oldShader);
+			}
+			
+			if (newShader > 0) {
+				// add new shader object
+				gl.glAttachShader(programId, newShader);
+			}
+		}
+		
+		return newShader;
+	}
+	
+	/* Assumes that the given source code is not empty. This does not
+	 * error checking on the compile status. 
+	 * 
+	 * Returns the id of the shader object, which will be new if objectId 
+	 * isn't positive. */
+	private static int compileShader(GL gl, int objectId, String[] sourceCode, int type) {
+		if (objectId <= 0)
+			objectId = gl.glCreateShader(type);
+		
+		int[] lineLengths = new int[sourceCode.length];
+		for (int i = 0; i < lineLengths.length; i++)
+			lineLengths[i] = sourceCode[i].length();
+		
+		gl.glShaderSource(objectId, sourceCode.length, sourceCode, lineLengths, 0);
+		gl.glCompileShader(objectId);
+		
+		return objectId;
+	}
+	
+	/* Return the string representation of the shader object's log. */
+	private static String getShaderLog(GL gl, int objectId) {
+		int[] logSize = new int[1];
+		gl.glGetShaderiv(objectId, GL.GL_INFO_LOG_LENGTH, logSize, 0);
+		
+		if (logSize[0] > 0) {
+			byte[] chars = new byte[logSize[0]];
+			gl.glGetShaderInfoLog(objectId, logSize[0], null, 0, chars, 0);
+			
+			return createString(chars, chars.length);
+		} else {
+			return "";
+		}
+	}
+	
+	/* Return true if the given shader object was compiled successfully. */
+	private static boolean isCompiled(GL gl, int objectId) {
+		int[] status = new int[1];
+		gl.glGetShaderiv(objectId, GL.GL_COMPILE_STATUS, status, 0);
+		return status[0] == GL.GL_TRUE;
+	}
+	
+	/* Return the string representation of the shader program's log. */
+	private static String getProgramLog(GL gl, int programId) {
+		int[] logSize = new int[1];
+		gl.glGetProgramiv(programId, GL.GL_INFO_LOG_LENGTH, logSize, 0);
+		
+		if (logSize[0] > 0) {
+			byte[] chars = new byte[logSize[0]];
+			gl.glGetProgramInfoLog(programId, logSize[0], null, 0, chars, 0);
+			
+			return createString(chars, chars.length);
+		} else {
+			return "";
+		}
+	}
+	
+	/* Return true if the given shader program was linked successfully. */
+	private static boolean isLinked(GL gl, int programId) {
+		int[] status = new int[1];
+		gl.glGetProgramiv(programId, GL.GL_LINK_STATUS, status, 0);
+		return status[0] == GL.GL_TRUE;
+	}
+	
+	/* Build a string from an opengl byte array. */
+	private static String createString(byte[] chars, int length) {
+		StringBuilder builder = new StringBuilder();
+		for (int i = 0; i < length; i++)
+			builder.append((char) chars[i]);
+		return builder.toString();
+	}
+}

src/com/ferox/renderer/impl/jogl/drivers/JoglGlslUniformResourceDriver.java

+package com.ferox.renderer.impl.jogl.drivers;
+
+import javax.media.opengl.GL;
+
+import com.ferox.renderer.impl.ResourceData;
+import com.ferox.renderer.impl.ResourceDriver;
+import com.ferox.renderer.impl.ResourceData.Handle;
+import com.ferox.renderer.impl.jogl.EnumUtil;
+import com.ferox.renderer.impl.jogl.JoglSurfaceFactory;
+import com.ferox.resource.GlslUniform;
+import com.ferox.resource.Resource;
+import com.ferox.resource.GlslUniform.UniformType;
+import com.ferox.resource.Resource.Status;
+
+/** JoglGlslUniformResourceDriver detects a uniform's index within
+ * its owning GlslProgram and verifies that its type and length are
+ * as expected.
+ * 
+ * @author Michael Ludwig
+ *
+ */
+public class JoglGlslUniformResourceDriver implements ResourceDriver {
+	/* Very simple handle, all it is, is the id for the uniform. */
+	private static class GlslUniformHandle implements Handle {
+		private int id;
+		
+		@Override
+		public int getId() {
+			return this.id;
+		}
+	}
+	
+	private JoglSurfaceFactory factory;
+	private boolean glslSupport;
+	
+	public JoglGlslUniformResourceDriver(JoglSurfaceFactory factory) {
+		this.factory = factory;
+		this.glslSupport = factory.getRenderer().getCapabilities().getGlslSupport();
+	}
+	
+	@Override
+	public void cleanUp(Resource resource, ResourceData data) {
+		// nothing to clean-up
+	}
+
+	@Override
+	public void update(Resource resource, ResourceData data, boolean fullUpdate) {
+		GL gl = this.factory.getGL();
+		
+		GlslUniformHandle handle = (GlslUniformHandle) data.getHandle();
+		if (handle == null) {
+			// check support
+			if (!this.glslSupport) {
+				data.setStatus(Status.ERROR);
+				data.setStatusMessage("GLSL is not supported on this hardware");
+				return; // abort
+			}
+			
+			handle = new GlslUniformHandle();
+			data.setHandle(handle);
+		}
+		
+		GlslUniform uniform = (GlslUniform) resource;
+		Handle program = this.factory.getRenderer().getHandle(uniform.getOwner());
+		
+		if (program == null) {
+			// program is bad, so all uniforms are invalid
+			data.setStatus(Status.ERROR);
+			data.setStatusMessage("Owning GlslProgram has a status of ERROR or CLEANED, cannot have a valid GlslUniform");
+		} else {
+			handle.id = gl.glGetUniformLocation(program.getId(), uniform.getName());
+			
+			if (handle.id < 0) {
+				// uniform doesn't exist in shader
+				data.setStatus(Status.ERROR);
+				data.setStatusMessage("Uniform with a name of " + uniform.getName() + " is not an active uniform in its glsl program");
+			} else {
+				// uniform name is exists, now we have to query and match other properties
+				int[] type = new int[1];
+				int[] size = new int[1];
+				gl.glGetActiveUniform(program.getId(), handle.id, 0, null, 0, size, 0, type, 0, null, 0);
+				
+				UniformType expectedType = EnumUtil.getUniformType(type[0]);
+				if (expectedType != uniform.getType()) {
+					data.setStatus(Status.ERROR);
+					data.setStatusMessage("Expected uniform type is " + expectedType + ", not " + uniform.getType());
+				} else if (size[0] != uniform.getLength()) {
+					data.setStatus(Status.ERROR);
+					data.setStatusMessage("Expected uniform length is " + size[0] + ", not " + uniform.getLength());
+				} else {
+					// everything else matches, so we're valid
+					data.setStatus(Status.OK);
+					data.setStatusMessage("");
+				}
+			}
+		}
+		
+		// just to finish things off
+		resource.clearDirtyDescriptor();
+	}
+}

src/com/ferox/renderer/impl/jogl/drivers/JoglShaderStateDriver.java

+package com.ferox.renderer.impl.jogl.drivers;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+
+import javax.media.opengl.GL;
+
+import com.ferox.renderer.impl.ResourceData.Handle;
+import com.ferox.renderer.impl.jogl.JoglSurfaceFactory;
+import com.ferox.renderer.impl.jogl.record.GlslShaderRecord;
+import com.ferox.renderer.impl.jogl.record.JoglStateRecord;
+import com.ferox.resource.GlslUniform;
+import com.ferox.state.GlslShader;
+import com.ferox.state.GlslShader.UniformBinding;
+
+public class JoglShaderStateDriver extends SingleStateDriver<GlslShader> {
+	private final IdentityHashMap<GlslUniform, GlslUniform> perFrameUniforms;
+	
+	public JoglShaderStateDriver(JoglSurfaceFactory factory) {
+		super(null, GlslShader.class, factory);
+		this.perFrameUniforms = new IdentityHashMap<GlslUniform, GlslUniform>();
+	}
+	
+	@Override
+	public void reset() {
+		super.reset();
+		// we use reset() as a proxy for each frame
+		// although it's really just each surface
+		this.perFrameUniforms.clear();
+	}
+
+	@Override
+	protected void apply(GL gl, JoglStateRecord record, GlslShader nextState) {
+		GlslShaderRecord sr = record.shaderRecord;
+		
+		if (nextState == null) {
+			// disable shaders and use the fixed-function pipeline
+			if (sr.glslProgramBinding != 0) {
+				gl.glUseProgram(0);
+				sr.glslProgramBinding = 0;
+			}
+		} else {
+			// bind the GlslProgram and set variables as needed
+			Handle p = this.factory.getRenderer().getHandle(nextState.getProgram());
+			if (p == null) {
+				// the program can't be used
+				if (sr.glslProgramBinding != 0) {
+					gl.glUseProgram(0);
+					sr.glslProgramBinding = 0;
+				}
+			} else {
+				// bind the program
+				if (sr.glslProgramBinding != p.getId()) {
+					sr.glslProgramBinding = p.getId();
+					gl.glUseProgram(sr.glslProgramBinding);
+				}
+				
+				// set the uniforms
+				List<UniformBinding> uniforms = nextState.getSetUniforms();
+				int size = uniforms.size();
+				for (int i = 0; i < size; i++) {
+					this.setUniform(gl, uniforms.get(i));
+				}
+			}
+		}
+	}
+	
+	private void setUniform(GL gl, UniformBinding binding) {
+		GlslUniform u = binding.getUniform();
+		
+		// determine if it's actually necessary to set the uniform
+		switch(u.getValueUpdatePolicy()) {
+		case MANUAL:
+			// only set it if we're dirty
+			if (binding.isDirty()) {
+				this.setUniformValue(gl, u, binding.getValue());
+				binding.setDirty(false);
+			}
+			break;
+		case PER_FRAME:
+			// only set it if we haven't seen since last reset()
+			if (!this.perFrameUniforms.containsKey(u)) {
+				this.setUniformValue(gl, u, binding.getValue());
+				this.perFrameUniforms.put(u, u);
+			}
+			break;
+		case PER_INSTANCE:
+			// set it all the time
+			this.setUniformValue(gl, u, binding.getValue());
+			break;
+		}
+	}
+	
+	private void setUniformValue(GL gl, GlslUniform uniform, Object value) {
+		Handle h = this.factory.getRenderer().getHandle(uniform);
+		if (h != null) {
+			switch(uniform.getType()) {
+			case BOOL: case INT: 
+			case SAMPLER_1D: case SAMPLER_1D_SHADOW:
+			case SAMPLER_2D: case SAMPLER_2D_SHADOW:
+			case SAMPLER_RECT:case SAMPLER_RECT_SHADOW: 
+			case SAMPLER_3D: case SAMPLER_CUBEMAP: {
+				// all of these use glUniform1i{v}
+				int[] val = (int[]) value;
+				if (val.length == 1)
+					gl.glUniform1i(h.getId(), val[0]);
+				else
+					gl.glUniform1iv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case BOOL_VEC2: case INT_VEC2: {
+				int[] val = (int[]) value;
+				if (val.length == 1)
+					gl.glUniform2i(h.getId(), val[0], val[1]);
+				else
+					gl.glUniform2iv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case BOOL_VEC3: case INT_VEC3: {
+				int[] val = (int[]) value;
+				if (val.length == 1)
+					gl.glUniform3i(h.getId(), val[0], val[1], val[2]);
+				else
+					gl.glUniform3iv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case BOOL_VEC4: case INT_VEC4: {
+				int[] val = (int[]) value;
+				if (val.length == 1)
+					gl.glUniform4i(h.getId(), val[0], val[1], val[2], val[3]);
+				else
+					gl.glUniform4iv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case FLOAT: {
+				float[] val = (float[]) value;
+				if (val.length == 1)
+					gl.glUniform1f(h.getId(), val[0]);
+				else
+					gl.glUniform1fv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case FLOAT_VEC2: {
+				float[] val = (float[]) value;
+				if (val.length == 1)
+					gl.glUniform2f(h.getId(), val[0], val[1]);
+				else
+					gl.glUniform2fv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case FLOAT_VEC3: {
+				float[] val = (float[]) value;
+				if (val.length == 1)
+					gl.glUniform3f(h.getId(), val[0], val[1], val[2]);
+				else
+					gl.glUniform3fv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case FLOAT_VEC4: {
+				float[] val = (float[]) value;
+				if (val.length == 1)
+					gl.glUniform4f(h.getId(), val[0], val[1], val[2], val[3]);
+				else
+					gl.glUniform4fv(h.getId(), uniform.getLength(), val, 0);
+				break; }
+			case FLOAT_MAT2: {
+				float[] val = (float[]) value;
+				gl.glUniformMatrix2fv(h.getId(), uniform.getLength(), false, val, 0);
+				break; }
+			case FLOAT_MAT3: {
+				float[] val = (float[]) value;
+				gl.glUniformMatrix3fv(h.getId(), uniform.getLength(), false, val, 0);
+				break; }
+			case FLOAT_MAT4: {
+				float[] val = (float[]) value;
+				gl.glUniformMatrix4fv(h.getId(), uniform.getLength(), false, val, 0);
+				break; }
+			}
+		} // else ignore it
+	}
+}

src/com/ferox/renderer/impl/jogl/record/GlslShaderRecord.java

+package com.ferox.renderer.impl.jogl.record;
+
+public class GlslShaderRecord {
+	public boolean enableVertexProgramTwoSide = false;
+	public boolean enableVertexProgramPointSize = false;
+	
+	public int glslProgramBinding = 0;
+}

src/com/ferox/renderer/impl/jogl/record/JoglStateRecord.java

 	public final PackUnpackRecord packRecord;
 	public final PixelOpRecord pixelOpRecord;
 	public final RasterizationRecord rasterRecord;
+	public final GlslShaderRecord shaderRecord;
 	public final TextureRecord textureRecord;
 	public final VertexArrayRecord vertexArrayRecord;
 	
 		this.pixelOpRecord = new PixelOpRecord();
 		this.packRecord = new PackUnpackRecord();
 		this.rasterRecord = new RasterizationRecord();
+		this.shaderRecord = new GlslShaderRecord();
 		
 		int maxTU = Math.max(caps.getMaxFixedPipelineTextures(), Math.max(caps.getMaxVertexShaderTextures(), caps.getMaxFragmentShaderTextures()));
 		this.textureRecord = new TextureRecord(maxTU);

src/com/ferox/resource/BufferedGeometry.java

 			if (accessor.getElementSize() != 1 && accessor.getElementSize() != 2 && accessor.getElementSize() != 3 && accessor.getElementSize() != 4)
 				throw new IllegalArgumentException("VertexArray can only have an element size of 1, 2, 3, or 4; not: " + accessor.getElementSize());
 
-			this.texCoords.setItem(unit, new GeometryArray<T>(vas, this.getEffectiveDataType(vas, false), accessor, this.getNumElements(vas, accessor)));
+			this.vertexAttribs.setItem(unit, new GeometryArray<T>(vas, this.getEffectiveDataType(vas, false), accessor, this.getNumElements(vas, accessor)));
 		} else {
-			this.texCoords.setItem(unit, null);
+			this.vertexAttribs.setItem(unit, null);
 		}
 	}
 	

src/com/ferox/resource/GlslProgram.java

 package com.ferox.resource;
 
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
  * GlslPrograms are a fairly complicated resource for the Renderer
  * to deal with.  Here are a few guidelines:
  * - If either shader fails to compile, it should have a status of ERROR
- * - If it fails to link, it has a status of ERROR
- * - If valid, bound GlslVertexAttributes overlap attribute slots, it should
- *   have a status of ERROR
- * - Invalid attributes must be unbound
- * - Invalid GlslUniforms must be left attached, unless they conflict with the
- *   name of a valid uniform
+ * - Overlapping attribute slots cause a status of ERROR.
+ * - If it fails to link, it has a status of ERROR.  Uniforms and attributes
+ *   are left attached (uniforms status should be changed, though)
+ * - If it does link, uniforms and attributes not declared in the code
+ *   should be detached and cleaned-up
  * - Attributes and uniforms declared in the glsl code should be bound/attached
- *   on a success if they aren't already.
- * - Attached uniforms must have a status of ERROR if the program failed to
- *   link.
- * - Attached uniforms should be automatically updated and cleaned-up whenever the program
- *   is updated/cleaned-up.
+ *   on a success if they aren't already attached, when successfully linked.
+ *   
+ * Care should be given when using the uniforms and attributes attached
+ * to a program.  If the program undergoes significant source code change, it
+ * is likely that the uniforms will have been detached.  Only attach a Uniform
+ * or attribute if you know that they are present in the code (even with compile errors);
+ * if not, use the automatically attached uniforms after a successful update.
  * 
  * @author Michael Ludwig
  *
  */
+// FIXME: vertex attributes should start at 1, not 0
 public class GlslProgram implements Resource {
 	/** The dirty descriptor used by instances of GlslProgram. */
 	public static class GlslProgramDirtyDescriptor {
 	
 	private final Map<String, GlslUniform> attachedUniforms;
 	private final Map<String, GlslVertexAttribute> boundAttrs;
-	
+	private final BitSet usedAttributeSlots;
+
 	private final Map<String, GlslUniform> readOnlyUniforms;
 	private final Map<String, GlslVertexAttribute> readOnlyAttrs;
 	
+	
 	private final GlslProgramDirtyDescriptor dirty;
 	
 	private Object renderData;
 	
-	/** Create a new GlslProgram with empty source code. */
-	public GlslProgram() {
-		this(null, null);
-	}
-	
 	/** Create a new GlslProgram with the given vertex and fragment
 	 * shaders.  These string arrays will be passed directly into
 	 * setVertexShader() and setFragmentShader(). */
 		this.readOnlyUniforms = Collections.unmodifiableMap(this.attachedUniforms);
 		this.readOnlyAttrs = Collections.unmodifiableMap(this.boundAttrs);
 		
-		this.setVertexShader(vertex);
-		this.setFragmentShader(fragment);
+		this.usedAttributeSlots = new BitSet();
+		
+		this.setShaderCode(vertex, fragment);
 	}
 	
 	/** Functions identically to attachUniform() except that it is for GlslVertexAttributes.
-	 * This method does not check for overlap in binding slots, but if there is any overlap
-	 * at the time of a Renderer update(), this program must have its status set to ERROR. 
+	 * This also fails if the requested attribute cannot fit within the available slots.
 	 * 
 	 * Instead of length, however, bindingSlot is used for the attribute.  See GlslVertexAttribute
 	 * for how the binding slot is used. */
 		else
 			u = new GlslVertexAttribute(name, type, bindingSlot, this); // throws IllegalArgumentException/NullPointerException
 		
+		// check if they're used
+		int maxSlot = bindingSlot + type.getSlotCount();
+		for (int i = bindingSlot; i < maxSlot; i++) {
+			if (this.usedAttributeSlots.get(i))
+				throw new IllegalStateException("Requested attribute does not have enough room at the given slot: " + bindingSlot);
+		}
+		
+		// mark slots as used
+		for (int i = bindingSlot; i < maxSlot; i++)
+			this.usedAttributeSlots.set(i, true);
+		
 		this.boundAttrs.put(name, u);
 		this.dirty.attrsDirty = true;
 		return u;
 	 * 
 	 * Because a GlslVertexAttribute is not a resource, and is only 
 	 * a convenient packaging for the binding of glsl attributes, a Renderer
-	 * will unbind any attribute that is invalid. */
+	 * will unbind any attribute that is invalid (don't unbind if the
+	 * shader doesn't compile, since the attribute may be "valid"). */
 	public GlslVertexAttribute unbindAttribute(String name) {
 		if (name == null)
 			return null;
 		
 		GlslVertexAttribute a = this.boundAttrs.remove(name);
-		if (a != null)
+		if (a != null) {
+			// mark the used slots as unused
+			int maxSlot = a.getBindingSlot() + a.getType().getSlotCount();
+			for (int i = a.getBindingSlot(); i < maxSlot; i++)
+				this.usedAttributeSlots.set(i, false);
 			this.dirty.attrsDirty = true;
+		}
 		
 		return a;
 	}
 	/** Attach a new GlslUniform instance to this GlslProgram so that it will
 	 * be automatically updated and cleaned-up with this program instance.
 	 * 
-	 * It is possible for programmers to manually attach the uniforms, to 
+	 * It is allowed for programmers to manually attach the uniforms, to 
 	 * use the instances sooner, or they may let the Renderer attach all
 	 * uniforms detected by the glsl compiler.
 	 * 
 	 * 
 	 * When a glsl program is updated, if it links successfully, it should
 	 * attach any uniform that is present in the code, that's not already
-	 * been attached.  It should not detach invalid uniforms unless they
-	 * conflict with the name of a valid uniform.  In either case, invalid
-	 * uniforms must have their status set to ERROR.
+	 * been attached.  It will detach and clean-up unvalid uniforms. 
 	 * 
 	 * If a program is not linked successfully, all attached uniforms
 	 * must have a status of ERROR. */
 	 * This allows the given name to be used in an attachUniform()
 	 * call without having to worry about an IllegalStateException.
 	 * 
-	 * The uniform will be auto-matically cleaned-up the next time
-	 * the program is updated or cleaned-up and should no longer be
-	 * used in GlslShaders.  After that clean-up, its status will no
-	 * longer be updated by the program's.
-	 * 
 	 * The purpose of this method is to allow programmers to detach
 	 * uniforms that have a status of ERROR, or the for the Renderer
 	 * to remove erroneous uniforms conflicting with valid uniforms of
-	 * the same name. */
+	 * the same name.
+	 * 
+	 * When a uniform is detached, the detacher is responsible
+	 * for cleaning it up with the renderer. */
 	public GlslUniform detachUniform(String name) {
 		if (name == null)
 			return null;
 		return this.fragmentShader;
 	}
 	
-	/** Set the glsl code that executes during the vertex stage of the pipeline.
-	 * If source is null, or (after ignoring null elements) there are no source
-	 * lines, getVertexShader() will return an array of length 0.
+	/** Set the glsl code that executes during the vertex and fragment
+	 * stages of the pipeline.
 	 * 
-	 * If not, then all non-null elements will be copied into a new array.
-	 * This is to prevent accidental tampering, and to allow programmers to
-	 * assume that all String elements in the returned array will not be null. */
-	public void setVertexShader(String[] source) {
-		if (source == null) {
-			this.vertexShader = new String[0];
-		} else {
-			int nonNullCount = 0;
-			for (int i = 0; i < source.length; i++) {
-				if (source[i] != null)
-					nonNullCount++;
-			}
-			
-			this.vertexShader = new String[nonNullCount];
-			nonNullCount = 0;
-			for (int i = 0; i < source.length; i++) {
-				if (source[i] != null) {
-					this.vertexShader[nonNullCount++] = source[i];
-				}
-			}
-		}
+	 * This method will copy each of the arrays over into new arrays,
+	 * removing any null elements.  A null input array is treated like
+	 * a String[0].  
+	 * 
+	 * If after copying, both vertex and fragment shaders sources have
+	 * a length of 0, then an exception is thrown: at least one portion
+	 * of the pipeline must be replaced by the shader. */
+	public void setShaderCode(String[] vertex, String[] fragment) throws IllegalArgumentException {
+		vertex = compress(vertex);
+		fragment = compress(fragment);
 		
-		this.dirty.glslDirty = true;
-	}
-	
-	/** Set the glsl code that executes during the fragment stage of the pipeline.
-	 * If source is null, or (after ignoring null elements) there are no source
-	 * lines, getFragmentShader() will return an array of length 0.
-	 * 
-	 * If not, then all non-null elements will be copied into a new array.
-	 * This is to prevent accidental tampering, and to allow programmers to
-	 * assume that all String elements in the returned array will not be null. */
-	public void setFragmentShader(String[] source) {
-		if (source == null) {
-			this.fragmentShader = new String[0];
-		} else {
-			int nonNullCount = 0;
-			for (int i = 0; i < source.length; i++) {
-				if (source[i] != null)
-					nonNullCount++;
-			}
-			
-			this.fragmentShader = new String[nonNullCount];
-			nonNullCount = 0;
-			for (int i = 0; i < source.length; i++) {
-				if (source[i] != null) {
-					this.fragmentShader[nonNullCount++] = source[i];
-				}
-			}
-		}
+		if (vertex.length == 0 && fragment.length == 0)
+			throw new IllegalArgumentException("Must specify at least one non-empty shader source");
+		
+		this.vertexShader = vertex;
+		this.fragmentShader = fragment;
 		
 		this.dirty.glslDirty = true;
 	}
 	public void setResourceData(Object data) {
 		this.renderData = data;
 	}
+	
+	/* Utility to remove null strings from the source. */
+	private static String[] compress(String[] source) {
+		if (source == null) {
+			return new String[0];
+		} else {
+			int nonNullCount = 0;
+			for (int i = 0; i < source.length; i++) {
+				if (source[i] != null)
+					nonNullCount++;
+			}
+			
+			String[] res = new String[nonNullCount];
+			nonNullCount = 0;
+			for (int i = 0; i < source.length; i++) {
+				if (source[i] != null) {
+					res[nonNullCount++] = source[i];
+				}
+			}
+			
+			return res;
+		}
+	}
 }

src/com/ferox/resource/GlslUniform.java

 package com.ferox.resource;
 
-import org.openmali.vecmath.Matrix3f;
-import org.openmali.vecmath.Matrix4f;
-import org.openmali.vecmath.Vector2f;
-import org.openmali.vecmath.Vector2i;
-import org.openmali.vecmath.Vector3f;
-import org.openmali.vecmath.Vector4f;
 
 /** GlslUniform represents a "static" variable that is assigned
  * values from outside the execution of its owning GlslProgram.
 	 * 
 	 * SAMPLER_x refer to Texture accessors.  The values
 	 * for sampler variables are the texture units with which
-	 * to access a texture from. 
+	 * to access a texture from.  Textures bound to these units
+	 * should be colored textures.
 	 * 
-	 * The given types may also be used in a GlslProgram as
-	 * an array.  This difference is determined by the GlslUniform
-	 * based on its length value. */
+	 * SHADOW_SAMPLER_x are like samplers, except they are
+	 * intended to be used with depth textures.
+	 * 
+	 * BOOL_x types use int[], except that 0 is treated as false,
+	 * and anything else is treated like true.
+	 * 
+	 * For simplicity, all values assigned to a GlslUniform with
+	 * a GlslShader are arrays of primitive types (float[], or int[]). 
+	 * A single-valued uniform should just use an array of length 1.  
+	 * Vector or matrix types will use consecutive
+	 * primitives to form one uniform element within the array. 
+	 * 
+	 * Matrices are specified in column major order. */
 	public static enum UniformType {
-		SAMPLER_1D, 		/** Must use int, Integer, or int[]. */ 
-		SAMPLER_2D, 		/** Must use int, Integer, or int[]. */ 
-		SAMPLER_3D, 		/** Must use int, Integer, or int[]. */ 
-		SAMPLER_CUBEMAP, 	/** Must use int, Integer, or int[]. */ 
-		SAMPLER_RECT, 		/** Must use int, Integer, or int[]. */ 
-		FLOAT,			    /** Must use float, Float, or float[]. */ 
-		INT, 				/** Must use int, Integer, or int[]. */ 
-		BOOLEAN,          	/** Must use boolean, Boolean, or boolean[]. */  
-		VEC2F,    			/** Must use Vector2f or Vector2f[]. */  
-		VEC3F,    			/** Must use Vector3f or Vector3f[]. */   
-		VEC4F,    			/** Must use Vector4f or Vector4f[]. */  
-		VEC2I,    			/** Must use Vector2i or Vector2i[]. */  
-		MAT3F,    			/** Must use Matrix3f or Matrix3f[]. */  
-		MAT4F,    			/** Must use Matrix4f or Matrix4f[]. */  
+		SAMPLER_1D(1, int[].class), 
+		SAMPLER_2D(1, int[].class),	
+		SAMPLER_3D(1, int[].class),
+		SAMPLER_CUBEMAP(1, int[].class), 
+		SAMPLER_RECT(1, int[].class), 
+		SAMPLER_1D_SHADOW(1, int[].class), 
+		SAMPLER_2D_SHADOW(1, int[].class), 
+		SAMPLER_RECT_SHADOW(1, int[].class),
+		
+		FLOAT(1, float[].class), 
+		FLOAT_VEC2(2, float[].class),
+		FLOAT_VEC3(3, float[].class), 
+		FLOAT_VEC4(4, float[].class),
+		
+		INT(1, int[].class), 
+		INT_VEC2(2, int[].class),
+		INT_VEC3(3, int[].class),
+		INT_VEC4(4, int[].class),
+		
+		BOOL(1, int[].class),
+		BOOL_VEC2(2, int[].class),
+		BOOL_VEC3(3, int[].class),
+		BOOL_VEC4(4, int[].class),
+		
+		FLOAT_MAT2(4, float[].class),
+		FLOAT_MAT3(9, float[].class), 
+		FLOAT_MAT4(16, float[].class);
+		
+		private int primitiveCount;
+		private Class<?> type;
+		private UniformType(int primitiveCount, Class<?> type) { 
+			this.primitiveCount = primitiveCount; 
+			this.type = type;
+		}
+		
+		/** Return the class type of variables that can be assigned
+		 * to the GlslUniform of this type.  All of these types are
+		 * primitive arrays. */
+		public Class<?> getVariableType() { return this.type; }
+		
+		/** Return the number of primitive elements that are used in
+		 * each uniform.  For example, FLOAT_MAT3 requires 9 float primitives
+		 * per each matrix. */
+		public int getPrimitiveCount() { return this.primitiveCount; }
 	}
 	
 	/** To make the updates of a GlslUniform's value as fast as possible,
 		if (value == null)
 			return false;
 		
-		if (this.length > 1) {
-			switch(this.type) {
-			case BOOLEAN: return (value instanceof boolean[]) && ((boolean[]) value).length == this.length;
-			
-			case INT: case SAMPLER_1D: case SAMPLER_2D: case SAMPLER_3D: case SAMPLER_CUBEMAP: case SAMPLER_RECT:
-				return (value instanceof int[]) && ((int[]) value).length == this.length;
-				
-			case FLOAT: return (value instanceof float[]) && ((float[]) value).length == this.length;
-			
-			case MAT3F: return (value instanceof Matrix3f[]) && ((Matrix3f[]) value).length == this.length;
-			case MAT4F: return (value instanceof Matrix4f[]) && ((Matrix4f[]) value).length == this.length;
-			
-			case VEC2F: return (value instanceof Vector2f[]) && ((Vector2f[]) value).length == this.length;
-			case VEC2I: return (value instanceof Vector2i[]) && ((Vector2i[]) value).length == this.length;
-			case VEC3F: return (value instanceof Vector3f[]) && ((Vector3f[]) value).length == this.length;
-			case VEC4F: return (value instanceof Vector4f[]) && ((Vector4f[]) value).length == this.length;
-			}
-		} else {
-			switch(this.type) {
-			case BOOLEAN: return value instanceof Boolean;
-			
-			case INT: case SAMPLER_1D: case SAMPLER_2D: case SAMPLER_3D: case SAMPLER_CUBEMAP: case SAMPLER_RECT:
-				return value instanceof Integer;
-				
-			case FLOAT: return value instanceof Float;
-			
-			case MAT3F: return value instanceof Matrix3f;
-			case MAT4F: return value instanceof Matrix4f;
-			
-			case VEC2F: return value instanceof Vector2f;
-			case VEC2I: return value instanceof Vector2i;
-			case VEC3F: return value instanceof Vector3f;
-			case VEC4F: return value instanceof Vector4f;
-			}
+		Class<?> expectedType = this.type.getVariableType();
+		if (!value.getClass().equals(expectedType)) 
+			return false;
+		
+		int expectedLength = this.length * this.type.getPrimitiveCount();
+		if (expectedType.equals(float[].class)) {
+			return ((float[]) value).length == expectedLength;
+		} else if (expectedType.equals(int[].class)) {
+			return ((int[]) value).length == expectedLength;
 		}
 		
 		return false;

src/com/ferox/resource/GlslVertexAttribute.java

 	 * matrix types use 2, 3, or 4 consecutive attribute 
 	 * slots to fill all values of the attribute. */
 	public static enum AttributeType {
-		FLOAT, VEC2F, VEC3F, VEC4F, MAT2F, MAT3F, MAT4F
+		FLOAT(1), VEC2F(1), VEC3F(1), VEC4F(1), MAT2F(2), MAT3F(3), MAT4F(4);
+		
+		private int slotCount;
+		private AttributeType(int slotCount) { this.slotCount = slotCount; }
+		
+		/** Return the number of adjacent slots that an attribute uses up. */
+		public int getSlotCount() { return this.slotCount; }
 	}
 	
 	private final AttributeType type;

src/com/ferox/resource/util/GlslProgramIO.java

+package com.ferox.resource.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.ferox.resource.GlslProgram;
+
+/** This is a very simple utility that loads a GlslProgram from
+ * two input text files that store the glsl code for the program.
+ * 
+ * @author Michael Ludwig
+ *
+ */
+public class GlslProgramIO {
+	/** Load the two files as text files and return a GlslProgram that uses that
+	 * text as its vertex and fragment shaders.  If vertexShader is null, then
+	 * no vertex shader is used by the program.  Similarly if fragmentShader is null, no 
+	 * fragment shader is used for the GlslProgram.
+	 * 
+	 * Throws an IOException if any I/O problem occurs while loading. */
+	public static GlslProgram load(File vertexShader, File fragmentShader) throws IOException {
+		String[] vCode = (vertexShader == null ? null : readAll(vertexShader));
+		String[] fCode = (fragmentShader == null ? null : readAll(fragmentShader));
+		
+		return new GlslProgram(vCode, fCode);
+	}
+	
+	/* Utility to read all lines of a file and convert it to an array of strings. */
+	private static String[] readAll(File file) throws IOException {
+		InputStreamReader stream = new InputStreamReader(new FileInputStream(file));
+		BufferedReader reader = new BufferedReader(stream);
+		
+		List<String> lines = new ArrayList<String>();
+		String line;
+		while ((line = reader.readLine()) != null) {
+			// read each line and add it to the list
+			lines.add(line);
+		}
+		
+		reader.close();
+		return lines.toArray(new String[lines.size()]);
+	}
+}

src/com/ferox/scene/Fog.java

  *
  */
 public class Fog extends Leaf implements State, InfluenceAtom {
-	/** Equation used to compute the amount of fog between start and end distances. */
+	/** Equation used to compute the amount of fog between start and end distances. 
+	 * The exact coloring of LINEAR fog depends on the start and end values,
+	 * for EXP and EXP_SQUARED, density effects the coloring. */
 	public static enum FogEquation {
 		EXP, EXP_SQUARED, LINEAR
 	}

test/com/ferox/BasicApplication.java

 package com.ferox;
 
-import java.awt.Component;
 import java.awt.Frame;
 import java.nio.FloatBuffer;
 import java.nio.IntBuffer;
 		}
 	}
 	
+	/** Called after window has been configured with pass. */
 	protected abstract SceneElement buildScene(Renderer renderer, ViewNode view);
 		
 	/** Return a RenderQueue that will be used with this application's
 		this.pass = new BasicRenderPass(null, v, this.createQueue(), false);
 		
 		//this.window = renderer.createFullscreenSurface(new DisplayOptions(), 800, 600);
-		this.window = renderer.createWindowSurface(this.createOptions(), 10, 10, 640, 480, true, false);
+		this.window = renderer.createWindowSurface(this.createOptions(), 10, 10, 640, 480, false, false);
 		this.window.addRenderPass(this.pass);
 		this.window.setTitle(this.getClass().getSimpleName());
 		this.window.setClearColor(new Color(.5f, .5f, .5f, 1f));
 		
 		// somewhat lame to get input working for now
 		Frame f = (Frame) this.window.getWindowImpl();
-		Component child = f.getComponent(0);
-		this.configureInputHandling(child, viewTrans);
+		this.configureInputHandling(f, viewTrans);
 	}
 	
 	@Override

test/com/ferox/scene/CubeTest.java

-package com.ferox.scene;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.FloatBuffer;
-import java.nio.IntBuffer;
-
-import org.openmali.vecmath.Vector3f;
-
-import com.ferox.BasicApplication;
-import com.ferox.math.BoundSphere;
-import com.ferox.math.Color;
-import com.ferox.renderer.DisplayOptions;
-import com.ferox.renderer.Renderer;
-import com.ferox.renderer.TextureSurface;
-import com.ferox.renderer.View;
-import com.ferox.renderer.DisplayOptions.DepthFormat;
-import com.ferox.renderer.DisplayOptions.PixelFormat;
-import com.ferox.renderer.util.BasicRenderPass;
-import com.ferox.resource.Geometry;
-import com.ferox.resource.TextureImage;
-import com.ferox.resource.VertexArray;
-import com.ferox.resource.VertexArrayGeometry;
-import com.ferox.resource.BufferedGeometry.PolygonType;
-import com.ferox.resource.TextureImage.TextureTarget;
-import com.ferox.resource.util.TextureIO;
-import com.ferox.scene.Fog.FogEquation;
-import com.ferox.state.Appearance;
-import com.ferox.state.DepthTest;
-import com.ferox.state.FogReceiver;
-import com.ferox.state.LightReceiver;
-import com.ferox.state.Material;
-import com.ferox.state.Texture;
-import com.ferox.state.FogReceiver.FogCoordSource;
-import com.ferox.state.State.PixelTest;
-import com.ferox.state.State.Quality;
-import com.ferox.state.Texture.EnvMode;
-import com.ferox.state.Texture.TexCoordGen;
-import com.sun.opengl.util.BufferUtil;
-
-public class CubeTest extends BasicApplication {
-	public static final boolean DEBUG = false;
-	public static final boolean USE_VBO = true;
-	public static final boolean RANDOM_PLACEMENT = true;
-	
-	public static final int NUM_CUBES = 10000;
-	public static final int BOUNDS = 100;
-	
-	public static final Color bgColor = new Color(0f, 0f, 0f);
-	
-	protected Geometry geom;
-	protected TextureSurface sceneDepth;
-	
-	public static void main(String[] args) {
-		new CubeTest(DEBUG).run();
-	}
-	
-	public CubeTest(boolean debug) {
-		super(debug);
-	}
-
-	@Override
-	protected SceneElement buildScene(Renderer renderer, ViewNode view) {
-		view.getView().setPerspective(60f, 1f, 1f, 100f);
-		view.getLocalTransform().getTranslation().z = 2f * BOUNDS;
-		
-		Group root = new Group();
-		root.add(view);
-		
-		Light spotLight = new SpotLight();
-		
-		spotLight.setLocalBounds(new BoundSphere(BOUNDS));
-		spotLight.getLocalTransform().getTranslation().set(0f, 0f, 0f);
-		view.add(spotLight);
-		
-		Light directionLight = new DirectionLight(new Vector3f(-1f, -1f, -1f));
-		directionLight.setLocalBounds(new BoundSphere(BOUNDS));
-		root.add(directionLight);
-		
-		Fog fog = new Fog(bgColor, 10f, view.getView().getFrustumFar(), 1f, FogEquation.LINEAR, Quality.BEST);
-		fog.setLocalBounds(new BoundSphere(BOUNDS));
-		root.add(fog);
-		
-		Appearance[] apps = this.createAppearances(renderer);
-		this.geom = buildCube(renderer, 2f, USE_VBO);
-		
-		// vars for regular gridding
-		int sideCubeCount = (int) (Math.ceil(Math.pow(NUM_CUBES, 1.0 / 3.0)));
-		float scale = BOUNDS / (float) sideCubeCount;
-		int x = 0;
-		int y = 0;
-		int z = 0;
-		
-		for (int i = 0; i < NUM_CUBES; i++) {
-			Shape shape = new Shape(geom, apps[i % apps.length]);
-			shape.setLocalBounds(new BoundSphere());
-			Vector3f pos = shape.getLocalTransform().getTranslation();
-			
-			if (RANDOM_PLACEMENT) {
-				if (i != 0) {
-					// randomly place all but the 1st cube
-					pos.x = (float) (Math.random() * BOUNDS - BOUNDS / 2f);
-					pos.y = (float) (Math.random() * BOUNDS - BOUNDS / 2f);
-					pos.z = (float) (Math.random() * BOUNDS - BOUNDS / 2f);
-				}
-			} else {
-				pos.x = scale * x - BOUNDS / 2f;
-				pos.y = scale * y - BOUNDS / 2f;
-				pos.z = scale * z - BOUNDS / 2f;
-				
-				x++;
-				if (x >= sideCubeCount) {
-					x = 0;
-					y++;
-					if (y >= sideCubeCount) {
-						y = 0;
-						z++;
-					}
-				}
-			}
-			
-			root.add(shape);
-		}
-		
-		this.sceneDepth = this.setupSceneDepthSurface(renderer, root, view.getView());
-		return root;
-	}
-	
-	@Override
-	public boolean render(Renderer renderer) {
-		renderer.queueRender(this.sceneDepth);
-		return super.render(renderer);
-	}
-	
-	private Geometry buildSquare(Renderer renderer, float left, float right, float bottom, float top) {
-		// make a cube face
-		FloatBuffer verts = BufferUtil.newFloatBuffer(8).put( new float[] {left, bottom,
-						 												   right, bottom,
-						 												   right, top,
-						 												   left, top});
-		FloatBuffer tcs = BufferUtil.newFloatBuffer(8).put( new float[] {0f, 0f,
-																		 1f, 0f,
-																		 1f, 1f,
-																		 0f, 1f});
-		IntBuffer ibs = BufferUtil.newIntBuffer(4).put(new int[] {0, 1, 2, 3});
-		
-		VertexArrayGeometry square = new VertexArrayGeometry(verts, new VertexArray(2), ibs, new VertexArray(1), PolygonType.QUADS);
-		square.setTextureCoordinates(0, tcs, new VertexArray(2));
-		renderer.requestUpdate(square, false);
-		
-		return square;
-	}
-	
-	private TextureSurface setupSceneDepthSurface(Renderer renderer, SceneElement root, View view) {
-		TextureSurface sceneDepth = renderer.createTextureSurface(new DisplayOptions(PixelFormat.RGB_24BIT, DepthFormat.DEPTH_24BIT), TextureTarget.T_2D, 
-																				     this.window.getWidth(), this.window.getHeight(), 1, 0, 0, false);
-		
-		sceneDepth.setClearColor(bgColor);
-		
-		BasicRenderPass depthPass = new BasicRenderPass(root, view);		
-		sceneDepth.addRenderPass(depthPass);
-		
-		// this pass will display the color and depth textures in the window
-		TextureImage depthTex = sceneDepth.getDepthBuffer();	
-		TextureImage colorTex = sceneDepth.getColorBuffer(0);
-		
-		DepthTest dt = new DepthTest();
-		dt.setTest(PixelTest.ALWAYS);
-		
-		Shape depthShape = new Shape(this.buildSquare(renderer, 0, 128, 0, 128), new Appearance(new Texture(depthTex), dt));
-		Shape colorShape = new Shape(this.buildSquare(renderer, 0, this.window.getWidth(), 0, this.window.getHeight()), 
-													  new Appearance(new Texture(colorTex), dt));	
-		
-		View ortho = new View();
-		ortho.setOrthogonalProjection(true);
-		ortho.setFrustum(0, this.window.getWidth(), 0, this.window.getHeight(), -1, 1);
-		
-		this.window.removeRenderPass(this.pass);
-		this.window.addRenderPass(new BasicRenderPass(colorShape, ortho));
-		this.window.addRenderPass(new BasicRenderPass(depthShape, ortho));
-		
-		return sceneDepth;
-	}
-	
-	private Appearance[] createAppearances(Renderer renderer) {
-		Appearance[] apps = new Appearance[4];
-		for (int i = 0; i < apps.length; i++)
-			apps[i] = this.createAppearance(renderer, i, apps.length);
-		return apps;
-	}
-	
-	private Appearance createAppearance(Renderer renderer, int i, int max) {
-		float percent = (float) i / max;
-		Material m;
-		
-		if (percent < .3333f) {
-			m = new Material(new Color(1f, percent, percent));
-		} else if (percent < .666667f) {
-			m = new Material(new Color(percent / 2f, 1f, percent / 2f));
-		} else {
-			m = new Material(new Color(percent / 3f, percent / 3f, 1f));
-		}
-
-		LightReceiver lr = null;
-		FogReceiver fr = null;
-		if (i % 2 == 0) {
-			lr = new LightReceiver();
-			m.setSmoothShaded(true);
-		} {
-			fr = new FogReceiver();
-			fr.setFogCoordinateSource(FogCoordSource.FRAGMENT_DEPTH);
-		}
-		
-		Texture t = null;
-		
-		if (i < max / 2) {
-			try {
-				TextureImage image = TextureIO.readTexture(new File("data/textures/squiggles.tga"));
-				renderer.requestUpdate(image, false);
-				t = new Texture(image, EnvMode.MODULATE, new Color(), TexCoordGen.NONE);
-			} catch (IOException io) {
-				throw new RuntimeException(io);
-			}
-		} else {
-			try {
-				TextureImage image = TextureIO.readTexture(new File("data/textures/grace_cube.dds"));
-				renderer.requestUpdate(image, false);
-				t = new Texture(image, EnvMode.MODULATE, new Color(), TexCoordGen.OBJECT);
-			} catch (IOException io) {
-				throw new RuntimeException(io);
-			}
-		}
-		
-		return new Appearance(m, lr, t, fr);
-	}
-}

test/com/ferox/scene/GlslTest.java

+package com.ferox.scene;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+import org.openmali.vecmath.Vector3f;
+
+import com.ferox.BasicApplication;
+import com.ferox.math.BoundSphere;
+import com.ferox.math.Color;
+import com.ferox.renderer.Renderer;
+import com.ferox.resource.Geometry;
+import com.ferox.resource.GlslProgram;
+import com.ferox.resource.GlslUniform;
+import com.ferox.resource.GlslVertexAttribute;
+import com.ferox.resource.TextureImage;
+import com.ferox.resource.VertexArray;
+import com.ferox.resource.VertexArrayGeometry;
+import com.ferox.resource.BufferedGeometry.PolygonType;
+import com.ferox.resource.GlslUniform.ValueUpdatePolicy;
+import com.ferox.resource.GlslVertexAttribute.AttributeType;
+import com.ferox.resource.util.TextureIO;
+import com.ferox.state.Appearance;
+import com.ferox.state.GlslShader;
+import com.ferox.state.LightReceiver;
+import com.ferox.state.Material;
+import com.ferox.state.MultiTexture;
+import com.ferox.state.Texture;
+import com.sun.opengl.util.BufferUtil;
+
+public class GlslTest extends BasicApplication {
+	public static final boolean DEBUG = false;
+	
+	public static void main(String[] args) {
+		new GlslTest(DEBUG).run();
+	}
+	
+	public GlslTest(boolean debug) {
+		super(debug);
+	}
+
+	@Override
+	protected SceneElement buildScene(Renderer renderer, ViewNode view) {
+		Group root = new Group();
+		
+		view.getLocalTransform().getTranslation().z = 20f;
+		root.add(view);
+		
+		Geometry cube = this.buildCube(4f);
+		renderer.requestUpdate(cube, true);
+		Appearance app = this.createGlslAppearance(renderer);
+		
+		Shape s = new Shape(cube, app);
+		s.setLocalBounds(new BoundSphere());
+		root.add(s);
+		
+		SpotLight light = new SpotLight(new Color(1f, 1f, 1f), new Color(1f, 0f, 0f), new Color());
+		light.setLocalBounds(new BoundSphere(20f));
+		light.getLocalTransform().getTranslation().set(-4f, 4f, 10f);
+		root.add(light);
+
+		Shape lightCube = new Shape(buildCube(renderer, .5f, false), 
+				new Appearance(new Material(light.getDiffuse())));
+		lightCube.setLocalTransform(light.getLocalTransform());
+		lightCube.setLocalBounds(new BoundSphere());
+		root.add(lightCube);
+
+		this.window.setVSyncEnabled(true);
+		return root;
+	}
+	
+	private Appearance createGlslAppearance(Renderer renderer) {
+		GlslProgram program = this.createProgram(renderer);
+		
+		GlslShader shader = new GlslShader(program);
+		program.getUniforms().get("diffuse").setValueUpdatePolicy(ValueUpdatePolicy.MANUAL);
+		shader.setUniform(program.getUniforms().get("diffuse"), new int[] {0});
+		program.getUniforms().get("specular").setValueUpdatePolicy(ValueUpdatePolicy.MANUAL);
+		shader.setUniform(program.getUniforms().get("specular"), new int[] {1});
+		program.getUniforms().get("normal").setValueUpdatePolicy(ValueUpdatePolicy.MANUAL);
+		shader.setUniform(program.getUniforms().get("normal"), new int[] {2});
+		
+		TextureImage diffuse = null;
+		TextureImage specular = null;
+		TextureImage normal = null;
+		
+		try {
+			diffuse = TextureIO.readTexture(new File("data/textures/wall_diffuse.png"));
+			specular = TextureIO.readTexture(new File("data/textures/wall_specular.png"));
+			normal = TextureIO.readTexture(new File("data/textures/wall_normal.png"));
+			
+			renderer.requestUpdate(diffuse, true);
+			renderer.requestUpdate(specular, true);
+			renderer.requestUpdate(normal, true);
+		} catch (IOException io) {
+			// fail
+			throw new RuntimeException(io);
+		}
+		
+		MultiTexture textures = new MultiTexture(new Texture(diffuse), new Texture(specular), new Texture(normal));
+		
+		return new Appearance(textures, shader, new LightReceiver());
+	}
+	
+	private GlslProgram createProgram(Renderer renderer) {
+		String[] vertexShader = {
+				"attribute vec3 tangent;",
+				"attribute vec3 bitangent;",
+
+				"varying vec3 half_vector;",
+				"varying vec3 light_dir;",
+
+				"varying vec3 tan;",
+				"varying vec3 bitan;",
+				"varying vec3 nm;",
+
+				"void main() {",
+					"gl_Position = gl_ModelViewMatrix * gl_Vertex;",
+					"gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;",
+
+					"tan = tangent;",
+					"bitan = bitangent;",
+					"nm = gl_Normal;",
+
+					"light_dir = gl_LightSource[0].position.xyz - gl_Position.xyz;",
+					"half_vector = gl_LightSource[0].halfVector.xyz;",
+
+					"gl_Position = gl_ProjectionMatrix * gl_Position;",
+				"}"
+			};
+
+		String[] fragmentShader = {
+			"uniform sampler2D diffuse;",
+			"uniform sampler2D normal;",
+			"uniform sampler2D specular;", 
+
+			"varying vec3 half_vector;",
+			"varying vec3 light_dir;",
+
+			"varying vec3 tan;",
+			"varying vec3 bitan;",
+			"varying vec3 nm;",
+
+			"void main() {",
+				"mat3 to_eye = gl_NormalMatrix * mat3(tan, bitan, nm);",
+				"vec3 norm = to_eye * (texture2D(normal, gl_TexCoord[0].st).grb * 2.0 - 1.0);",
+				"vec3 baseColor = texture2D(diffuse, gl_TexCoord[0].st).rgb;",
+				"vec3 lightVector = normalize(light_dir);",
+				"float nxDir = max(.1, dot(norm, lightVector));",
+				"vec4 diffuse = gl_LightSource[0].diffuse * nxDir;",
+
+				"float specularPower = 0.0;",
+				"if (nxDir > 0.0) {",
+					"lightVector = normalize(half_vector);",
+					"float nxHalf = max(0.0, dot(norm, lightVector));",
+					"specularPower = min(1.0, pow(nxHalf, gl_FrontMaterial.shininess));",
+				"}",
+
+				"vec4 spec = (gl_LightSource[0].specular * vec4(texture2D(specular, gl_TexCoord[0].st).rgb, 1.0)) * specularPower;",
+				"gl_FragColor = (diffuse * vec4(baseColor.rgb, 1.0)) + spec;",
+			"}"
+		};
+
+		GlslProgram program = new GlslProgram(vertexShader, fragmentShader);
+		program.bindAttribute("tangent", AttributeType.VEC3F, 1);
+		program.bindAttribute("bitangent", AttributeType.VEC3F, 2);
+
+		renderer.requestUpdate(program, true);
+		renderer.flushRenderer(null);
+
+		System.out.println(renderer.getStatus(program));
+		System.out.println(renderer.getStatusMessage(program));
+
+		for (GlslUniform u: program.getUniforms().values()) {
+			System.out.println("Uniform: " + u.getName() + " " + u.getType() + " " + u.getLength());
+		}
+
+		for (GlslVertexAttribute a: program.getAttributes().values()) {
+			System.out.println("Attribute: " + a.getName() + " " + a.getType() + " " + a.getBindingSlot());
+		}
+
+		return program;
+	}
+	
+	private Geometry buildCube(float side) {
+		float[] v = new float[72];
+		float[] n = new float[72];
+		float[] t = new float[48];
+		float[] tan = new float[72];
+		float[] btan = new float[72];
+		
+		// front
+		v[0] = 1f; v[1] = 1f; v[2] = 1f; n[0] = 0f; n[1] = 0f; n[2] = 1f; t[0] = 1f; t[1] = 1f;
+		v[3] = -1f; v[4] = 1f; v[5] = 1f; n[3] = 0f; n[4] = 0f; n[5] = 1f; t[2] = 0f; t[3] = 1f;
+		v[6] = -1f; v[7] = -1f; v[8] = 1f; n[6] = 0f; n[7] = 0f; n[8] = 1f; t[4] = 0f; t[5] = 0f;
+		v[9] = 1f; v[10] = -1f; v[11] = 1f; n[9] = 0f; n[10] = 0f; n[11] = 1f; t[6] = 1f; t[7] = 0f;
+		//back
+		v[12] = -1f; v[13] = -1f; v[14] = -1f; n[12] = 0f; n[13] = 0f; n[14] = -1f; t[8] = 1f; t[9] = 1f;
+		v[21] = 1f; v[22] = -1f; v[23] = -1f; n[21] = 0f; n[22] = 0f; n[23] = -1f; t[10] = 0f; t[11] = 1f;
+		v[18] = 1f; v[19] = 1f; v[20] = -1f; n[18] = 0f; n[19] = 0f; n[20] = -1f; t[12] = 0f; t[13] = 0f;
+		v[15] = -1f; v[16] = 1f; v[17] = -1f; n[15] = 0f; n[16] = 0f; n[17] = -1f; t[14] = 1f; t[15] = 0f;
+		//right
+		v[24] = 1f; v[25] = 1f; v[26] = -1f; n[24] = 1f; n[25] = 0f; n[26] = 0f; t[16] = 1f; t[17] = 1f;
+		v[27] = 1f; v[28] = 1f; v[29] = 1f; n[27] = 1f; n[28] = 0f; n[29] = 0f; t[18] = 0f; t[19] = 1f;
+		v[30] = 1f; v[31] = -1f; v[32] = 1f; n[30] = 1f; n[31] = 0f; n[32] = 0f; t[20] = 0f; t[21] = 0f;
+		v[33] = 1f; v[34] = -1f; v[35] = -1f; n[33] = 1f; n[34] = 0f; n[35] = 0f; t[22] = 1f; t[23] = 0f;
+		//left
+		v[36] = -1f; v[37] = -1f; v[38] = 1f; n[36] = -1f; n[37] = 0f; n[38] = 0f; t[24] = 1f; t[25] = 1f;
+		v[45] = -1f; v[46] = -1f; v[47] = -1f; n[45] = -1f; n[46] = 0f; n[47] = 0f; t[26] = 0f; t[27] = 1f;
+		v[42] = -1f; v[43] = 1f; v[44] = -1f; n[42] = -1f; n[43] = 0f; n[44] = 0f; t[28] = 0f; t[29] = 0f;
+		v[39] = -1f; v[40] = 1f; v[41] = 1f; n[39] = -1f; n[40] = 0f; n[41] = 0f; t[30] = 1f; t[31] = 0f;
+		//top
+		v[48] = -1f; v[49] = 1f; v[50] = -1f; n[48] = 0f; n[49] = 1f; n[50] = 0f; t[32] = 1f; t[33] = 1f;
+		v[57] = 1f; v[58] = 1f; v[59] = -1f; n[57] = 0f; n[58] = 1f; n[59] = 0f; t[34] = 0f; t[35] = 1f;
+		v[54] = 1f; v[55] = 1f; v[56] = 1f; n[54] = 0f; n[55] = 1f; n[56] = 0f; t[36] = 0f; t[37] = 0f;
+		v[51] = -1f; v[52] = 1f; v[53] = 1f; n[51] = 0f; n[52] = 1f; n[53] = 0f; t[38] = 1f; t[39] = 0f;
+		//bottom
+		v[60] = 1f; v[61] = -1f; v[62] = 1f; n[60] = 0f; n[61] = -1f; n[62] = 0f; t[40] = 1f; t[41] = 1f;
+		v[63] = -1f; v[64] = -1f; v[65] = 1f; n[63] = 0f; n[64] = -1f; n[65] = 0f; t[42] = 0f; t[43] = 1f;
+		v[66] = -1f; v[67] = -1f; v[68] = -1f; n[66] = 0f; n[67] = -1f; n[68] = 0f; t[44] = 0f; t[45] = 0f;
+		v[69] = 1f; v[70] = -1f; v[71] = -1f; n[69] = 0f; n[70] = -1f; n[71] = 0f; t[46] = 1f; t[47] = 0f;
+		
+		for (int i = 0; i < v.length; i++)
+			v[i] = v[i] * side / 2f;
+		
+		int[] i = new int[24];
+		for (int u = 0; u < 24; u++) {
+			i[u] = u;
+		}
+		
+		computeTangentBiTangent(v, t, i, tan, btan);
+		cleanTangentBiTangent(n, tan, btan);
+		
+		i = new int[48];
+		for (int u = 0; u < 6; u++) {
+			int t1 = u * 4;
+			int t2 = u * 4 + 1;
+			int t3 = u * 4 + 2;
+			int t4 = u * 4 + 3;
+			
+			i[u * 6] = t1;
+			i[u * 6 + 1] = t2;
+			i[u * 6 + 2] = t4;
+			
+			i[u * 6 + 3] = t2;
+			i[u * 6 + 4] = t3;
+			i[u * 6 + 5] = t4;
+		}
+		
+		FloatBuffer vb = BufferUtil.newFloatBuffer(v.length);
+		vb.put(v).rewind();
+		FloatBuffer nb = BufferUtil.newFloatBuffer(n.length);
+		nb.put(n).rewind();
+		FloatBuffer tb = BufferUtil.newFloatBuffer(t.length);
+		tb.put(t).rewind();
+		FloatBuffer tanb = BufferUtil.newFloatBuffer(tan.length);
+		tanb.put(tan).rewind();
+		FloatBuffer btanb = BufferUtil.newFloatBuffer(btan.length);
+		btanb.put(btan).rewind();
+		
+		IntBuffer ib = BufferUtil.newIntBuffer(i.length);
+		ib.put(i).rewind();
+		
+		VertexArrayGeometry geom = new VertexArrayGeometry(vb, new VertexArray(3), ib, new VertexArray(1), PolygonType.TRIANGLES);
+		geom.setNormals(nb, new VertexArray(3));
+		geom.setTextureCoordinates(0, tb, new VertexArray(2));
+		geom.setVertexAttributes(1, tanb, new VertexArray(3));
+		geom.setVertexAttributes(2, btanb, new VertexArray(3));
+
+		return geom;
+	}
+
+	private static void cleanTangentBiTangent(float[] normals, float[] tan, float[] bitan) {
+		Vector3f tp = new Vector3f();
+		Vector3f bp = new Vector3f();
+		Vector3f normal = new Vector3f();
+		Vector3f tangent = new Vector3f();
+		Vector3f bitangent = new Vector3f();
+		
+		float dot;
+		for (int i = 0; i < normals.length / 3; i++) {
+			normal.set(normals[i*3], normals[i*3+1], normals[i*3+2]);
+			tangent.set(tan[i*3], tan[i*3+1], tan[i*3+2]);
+			bitangent.set(bitan[i*3], bitan[i*3+1], bitan[i*3+2]);
+
+			dot = normal.dot(tangent);
+			tp.set(tangent);
+			tangent.scale(-dot, normal);
+			tp.add(tangent);
+			tp.normalize();
+			
+			tan[i*3] = tp.x;
+			tan[i*3+1] = tp.y;
+			tan[i*3+2] = tp.z;
+			
+			dot = tp.dot(bitangent);
+			tangent.scale(-dot, tp);
+			dot = normal.dot(bitangent);
+			tp.scale(-dot, normal);
+			
+			bp.set(bitangent);
+			bp.add(tp);
+			bp.add(tangent);
+			bp.normalize();
+			
+			bitan[i*3] = bp.x;
+			bitan[i*3+1] = bp.y;
+			bitan[i*3+2] = bp.z;
+		}
+	}
+	
+	private static void computeTangentBiTangent(float[] verts, float[] texcoords, int[] indices, float[] tan, float[] bitan) {
+		for (int i = 0; i < indices.length / 4; i++) {
+			computeForTriangle(indices[i*4], indices[i*4+1], indices[i*4+3], verts, texcoords, tan, bitan);
+			computeForTriangle(indices[i*4+1], indices[i*4], indices[i*4+2], verts, texcoords, tan, bitan);
+			computeForTriangle(indices[i*4+2], indices[i*4+1], indices[i*4+3], verts, texcoords, tan, bitan);
+			computeForTriangle(indices[i*4+3], indices[i*4], indices[i*4+2], verts, texcoords, tan, bitan);
+		}
+	}
+	
+	private static void computeForTriangle(int v0, int v1, int v2, float[] verts, float[] texcoords, float[] tan, float[] bitan) {
+		float s1, t1;
+		float s2, t2;
+		
+		Vector3f q1 = new Vector3f();
+		Vector3f q2 = new Vector3f();
+		
+		q1.x = verts[v1*3] - verts[v0*3];
+		q1.y = verts[v1*3 + 1] - verts[v0*3 + 1];
+		q1.z = verts[v1*3 + 2] - verts[v0*3 + 2];
+		
+		q2.x = verts[v2*3] - verts[v0*3];
+		q2.y = verts[v2*3 + 1] - verts[v0*3 + 1];
+		q2.z = verts[v2*3 + 2] - verts[v0*3 + 2];
+		
+		s1 = texcoords[v1*2] - texcoords[v0*2];
+		t1 = texcoords[v1*2+1] - texcoords[v0*2+1];
+		
+		s2 = texcoords[v2*2] - texcoords[v0*2];
+		t2 = texcoords[v2*2+1] - texcoords[v0*2+1];
+		
+		float scale = 1 / (s1*t2 - s2*t1);
+		
+		
+		// as of yet, unnormalized or guaranteed orthogonal
+		tan[v0*3] = scale * (t2 * q1.x - t1 * q2.x);
+		tan[v0*3+1] = scale * (t2 * q1.y - t1 * q2.y);
+		tan[v0*3+2] = scale * (t2 * q1.z - t1 * q2.z);
+		
+		bitan[v0*3] = scale * (-s2 * q1.x + s1 * q2.x);
+		bitan[v0*3+1] = scale * (-s2 * q1.y + s1 * q2.y);
+		bitan[v0*3+2] = scale * (-s2 * q1.z + s1 * q2.z);
+	}
+}

test/com/ferox/scene/RttCubeTest.java

+package com.ferox.scene;
+