Index: source/graphics/Material.h
===================================================================
--- source/graphics/Material.h	(revision 11919)
+++ source/graphics/Material.h	(working copy)
@@ -38,6 +38,12 @@
 
 	void SetDiffuseTexture(const CTexturePtr& texture);
 	const CTexturePtr& GetDiffuseTexture() const { return m_DiffuseTexture; }
+	
+	void SetNormalTexture(const CTexturePtr& texture);
+	const CTexturePtr& GetNormalTexture() const { return m_NormalTexture; }
+	
+	void SetSpecularTexture(const CTexturePtr& texture);
+	const CTexturePtr& GetSpecularTexture() const { return m_SpecularTexture; }
 
 	void SetShaderEffect(const CStr& effect);
 	CStrIntern GetShaderEffect() const { return m_ShaderEffect; }
@@ -50,6 +56,8 @@
 
 private:
 	CTexturePtr m_DiffuseTexture;
+	CTexturePtr m_NormalTexture;
+	CTexturePtr m_SpecularTexture;
 	CStrIntern m_ShaderEffect;
 	CShaderDefines m_ShaderDefines;
 	CShaderUniforms m_StaticUniforms;
Index: source/graphics/TerrainTextureManager.cpp
===================================================================
--- source/graphics/TerrainTextureManager.cpp	(revision 11919)
+++ source/graphics/TerrainTextureManager.cpp	(working copy)
@@ -143,6 +143,17 @@
 		// multiple extensions.
 		if(!tex_is_known_extension(pathnames[i]))
 			continue;
+		
+		// if this is a texture file, ensure that its basename does not
+		// end in "_norm" or "_spec", as those are not stand-alone texture maps
+		const Path base = pathnames[i].Basename();
+		const size_t underscore = base.string().find_last_of('_');
+		if(underscore != VfsPath::String::npos)
+		{
+			const Path p = base.string().substr(underscore);			
+			if (p == L"_norm" || p == L"_spec")
+				continue;
+		}
 
 		VfsPath pathnameXML = pathnames[i].ChangeExtension(L".xml");
 		CTerrainPropertiesPtr myprops;
Index: source/graphics/TerrainProperties.cpp
===================================================================
--- source/graphics/TerrainProperties.cpp	(revision 11919)
+++ source/graphics/TerrainProperties.cpp	(working copy)
@@ -103,6 +103,8 @@
 	ATTR(movementclass);
 	ATTR(angle);
 	ATTR(size);
+	ATTR(normalmap);
+	ATTR(specularmap);
 	#undef ELMT
 	#undef ATTR
 
@@ -154,6 +156,14 @@
 		{
 			m_MovementClass = attr.Value;
 		}
+		else if (attr.Name == attr_normalmap)
+		{
+			m_NormalMap = attr.Value;
+		}
+		else if (attr.Name == attr_specularmap)
+		{
+			m_SpecularMap = attr.Value;
+		}
 	}
 }
 
Index: source/graphics/ObjectEntry.h
===================================================================
--- source/graphics/ObjectEntry.h	(revision 11919)
+++ source/graphics/ObjectEntry.h	(working copy)
@@ -47,6 +47,8 @@
 	CObjectBase* m_Base;
 
 	VfsPath m_TextureName;
+	VfsPath m_NormalName;
+	VfsPath m_SpecularName;
 	// model name
 	VfsPath m_ModelName;
 	// colour (used when doing alpha-channel colouring, but not doing player-colour)
Index: source/graphics/mikktspace.h
===================================================================
--- source/graphics/mikktspace.h	(revision 0)
+++ source/graphics/mikktspace.h	(revision 0)
@@ -0,0 +1,146 @@
+/** \file mikktspace/mikktspace.h
+ *  \ingroup mikktspace
+ */
+/**
+ *  Copyright (C) 2011 by Morten S. Mikkelsen
+ *
+ *  This software is provided 'as-is', without any express or implied
+ *  warranty.  In no event will the authors be held liable for any damages
+ *  arising from the use of this software.
+ *
+ *  Permission is granted to anyone to use this software for any purpose,
+ *  including commercial applications, and to alter it and redistribute it
+ *  freely, subject to the following restrictions:
+ *
+ *  1. The origin of this software must not be misrepresented; you must not
+ *     claim that you wrote the original software. If you use this software
+ *     in a product, an acknowledgment in the product documentation would be
+ *     appreciated but is not required.
+ *  2. Altered source versions must be plainly marked as such, and must not be
+ *     misrepresented as being the original software.
+ *  3. This notice may not be removed or altered from any source distribution.
+ */
+
+#ifndef __MIKKTSPACE_H__
+#define __MIKKTSPACE_H__
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Author: Morten S. Mikkelsen
+ * Version: 1.0
+ *
+ * The files mikktspace.h and mikktspace.c are designed to be
+ * stand-alone files and it is important that they are kept this way.
+ * Not having dependencies on structures/classes/libraries specific
+ * to the program, in which they are used, allows them to be copied
+ * and used as is into any tool, program or plugin.
+ * The code is designed to consistently generate the same
+ * tangent spaces, for a given mesh, in any tool in which it is used.
+ * This is done by performing an internal welding step and subsequently an order-independent evaluation
+ * of tangent space for meshes consisting of triangles and quads.
+ * This means faces can be received in any order and the same is true for
+ * the order of vertices of each face. The generated result will not be affected
+ * by such reordering. Additionally, whether degenerate (vertices or texture coordinates)
+ * primitives are present or not will not affect the generated results either.
+ * Once tangent space calculation is done the vertices of degenerate primitives will simply
+ * inherit tangent space from neighboring non degenerate primitives.
+ * The analysis behind this implementation can be found in my master's thesis
+ * which is available for download --> http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf
+ * Note that though the tangent spaces at the vertices are generated in an order-independent way,
+ * by this implementation, the interpolated tangent space is still affected by which diagonal is
+ * chosen to split each quad. A sensible solution is to have your tools pipeline always
+ * split quads by the shortest diagonal. This choice is order-independent and works with mirroring.
+ * If these have the same length then compare the diagonals defined by the texture coordinates.
+ * XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin
+ * and also quad triangulator plugin.
+ */
+
+
+typedef int tbool;
+typedef struct SMikkTSpaceContext SMikkTSpaceContext;
+
+typedef struct
+{
+	// Returns the number of faces (triangles/quads) on the mesh to be processed.
+	int (*m_getNumFaces)(const SMikkTSpaceContext * pContext);
+
+	// Returns the number of vertices on face number iFace
+	// iFace is a number in the range {0, 1, ..., getNumFaces()-1}
+	int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext * pContext, const int iFace);
+
+	// returns the position/normal/texcoord of the referenced face of vertex number iVert.
+	// iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads.
+	void (*m_getPosition)(const SMikkTSpaceContext * pContext, float fvPosOut[], const int iFace, const int iVert);
+	void (*m_getNormal)(const SMikkTSpaceContext * pContext, float fvNormOut[], const int iFace, const int iVert);
+	void (*m_getTexCoord)(const SMikkTSpaceContext * pContext, float fvTexcOut[], const int iFace, const int iVert);
+
+	// either (or both) of the two setTSpace callbacks can be set.
+	// The call-back m_setTSpaceBasic() is sufficient for basic normal mapping.
+
+	// This function is used to return the tangent and fSign to the application.
+	// fvTangent is a unit length vector.
+	// For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level.
+	// bitangent = fSign * cross(vN, tangent);
+	// Note that the results are returned unindexed. It is possible to generate a new index list
+	// But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results.
+	// DO NOT! use an already existing index list.
+	void (*m_setTSpaceBasic)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert);
+
+	// This function is used to return tangent space results to the application.
+	// fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their
+	// true magnitudes which can be used for relief mapping effects.
+	// fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent.
+	// However, both are perpendicular to the vertex normal.
+	// For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level.
+	// fSign = bIsOrientationPreserving ? 1.0f : (-1.0f);
+	// bitangent = fSign * cross(vN, tangent);
+	// Note that the results are returned unindexed. It is possible to generate a new index list
+	// But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results.
+	// DO NOT! use an already existing index list.
+	void (*m_setTSpace)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT,
+						const tbool bIsOrientationPreserving, const int iFace, const int iVert);
+} SMikkTSpaceInterface;
+
+struct SMikkTSpaceContext
+{
+	SMikkTSpaceInterface * m_pInterface;	// initialized with callback functions
+	void * m_pUserData;						// pointer to client side mesh data etc. (passed as the first parameter with every interface call)
+};
+
+// these are both thread safe!
+tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext);	// Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled)
+tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold);
+
+
+// To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the
+// normal map sampler must use the exact inverse of the pixel shader transformation.
+// The most efficient transformation we can possibly do in the pixel shader is
+// achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN.
+// pixel shader (fast transform out)
+// vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN );
+// where vNt is the tangent space normal. The normal map sampler must likewise use the
+// interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader.
+// sampler does (exact inverse of pixel shader):
+// float3 row0 = cross(vB, vN);
+// float3 row1 = cross(vN, vT);
+// float3 row2 = cross(vT, vB);
+// float fSign = dot(vT, row0)<0 ? -1 : 1;
+// vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) );
+// where vNout is the sampled normal in some chosen 3D space.
+//
+// Should you choose to reconstruct the bitangent in the pixel shader instead
+// of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also.
+// Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of
+// quads as your renderer then problems will occur since the interpolated tangent spaces will differ
+// eventhough the vertex level tangent spaces match. This can be solved either by triangulating before
+// sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier.
+// However, this must be used both by the sampler and your tools/rendering pipeline.
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
Index: source/graphics/weldmesh.cpp
===================================================================
--- source/graphics/weldmesh.cpp	(revision 0)
+++ source/graphics/weldmesh.cpp	(revision 0)
@@ -0,0 +1,196 @@
+#include "precompiled.h"
+
+/**
+ *  Copyright (C) 2011 by Morten S. Mikkelsen
+ *
+ *  This software is provided 'as-is', without any express or implied
+ *  warranty.  In no event will the authors be held liable for any damages
+ *  arising from the use of this software.
+ *
+ *  Permission is granted to anyone to use this software for any purpose,
+ *  including commercial applications, and to alter it and redistribute it
+ *  freely, subject to the following restrictions:
+ *
+ *  1. The origin of this software must not be misrepresented; you must not
+ *     claim that you wrote the original software. If you use this software
+ *     in a product, an acknowledgment in the product documentation would be
+ *     appreciated but is not required.
+ *  2. Altered source versions must be plainly marked as such, and must not be
+ *     misrepresented as being the original software.
+ *  3. This notice may not be removed or altered from any source distribution.
+ */
+
+
+#include "weldmesh.h"
+#include <string.h>
+#include <assert.h>
+
+#ifdef __APPLE__
+#include <stdlib.h>  /* OSX gets its malloc stuff through here */
+#else
+#include <malloc.h> 
+#endif
+
+static void MergeVertsFast(int * piCurNrUniqueVertices, int * piRemapTable, float * pfVertexDataOut, int * piVertexIDs,
+			  const float pfVertexDataIn[], const int iNrVerticesIn, const int iFloatsPerVert,
+			  const int iL_in, const int iR_in, const int iChannelNum);
+
+int WeldMesh(int * piRemapTable, float * pfVertexDataOut,
+			  const float pfVertexDataIn[], const int iNrVerticesIn, const int iFloatsPerVert)
+{
+	int iUniqueVertices = 0, i=0;
+	int * piVertexIDs = NULL;
+	if(iNrVerticesIn<=0) return 0;
+
+
+	iUniqueVertices = 0;
+	piVertexIDs = (int *) malloc(sizeof(int)*iNrVerticesIn);
+	if(piVertexIDs!=NULL)
+	{
+		for(i=0; i<iNrVerticesIn; i++)
+		{
+			piRemapTable[i] = -1;
+			piVertexIDs[i] = i;
+		}
+
+		MergeVertsFast(&iUniqueVertices, piRemapTable, pfVertexDataOut, piVertexIDs,
+										 pfVertexDataIn, iNrVerticesIn, iFloatsPerVert, 0, iNrVerticesIn-1, 0);
+
+		free(piVertexIDs);
+
+		// debug check
+		for(i=0; i<iUniqueVertices; i++)
+			assert(piRemapTable[i]>=0);
+	}
+
+	return iUniqueVertices;
+}
+
+
+
+
+
+static void MergeVertsFast(int * piCurNrUniqueVertices, int * piRemapTable, float * pfVertexDataOut, int * piVertexIDs,
+			  const float pfVertexDataIn[], const int iNrVerticesIn, const int iFloatsPerVert,
+			  const int iL_in, const int iR_in, const int iChannelNum)
+{
+	const int iCount = iR_in-iL_in+1;
+	int l=0;
+	float fMin, fMax, fAvg;
+	assert(iCount>0);
+	// make bbox
+	fMin = pfVertexDataIn[ piVertexIDs[iL_in]*iFloatsPerVert + iChannelNum]; fMax = fMin;
+	for(l=(iL_in+1); l<=iR_in; l++)
+	{
+		const int index = piVertexIDs[l]*iFloatsPerVert + iChannelNum;
+		const float fVal = pfVertexDataIn[index];
+		if(fMin>fVal) fMin=fVal;
+		else if(fMax<fVal) fMax=fVal;
+	}
+
+	// terminate recursion when the separation/average value
+	// is no longer strictly between fMin and fMax values.
+	fAvg = 0.5f*(fMax + fMin);
+	if(fAvg<=fMin || fAvg>=fMax || iCount==1)
+	{
+		if((iChannelNum+1) == iFloatsPerVert || iCount==1)	// we are done, weld by hand
+		{
+			int iUniqueNewVertices = 0;
+			float * pfNewUniVertsOut = &pfVertexDataOut[ piCurNrUniqueVertices[0]*iFloatsPerVert ];
+
+			for(l=iL_in; l<=iR_in; l++)
+			{
+				const int index = piVertexIDs[l]*iFloatsPerVert;
+
+				int iFound = 0;	// didn't find copy yet.
+				int l2=0;
+				while(l2<iUniqueNewVertices && iFound==0)
+				{
+					const int index2 = l2*iFloatsPerVert;
+
+					int iAllSame = 1;
+					int c=0;
+					while(iAllSame!=0 && c<iFloatsPerVert)
+					{
+						iAllSame &= (pfVertexDataIn[index+c] == pfNewUniVertsOut[index2+c] ? 1 : 0);
+						++c;
+					}
+
+					iFound = iAllSame;
+					if(iFound==0) ++l2;
+				}
+				
+				// generate new entry
+				if(iFound==0)
+				{
+					memcpy(pfNewUniVertsOut+iUniqueNewVertices*iFloatsPerVert, pfVertexDataIn+index, sizeof(float)*iFloatsPerVert);
+					++iUniqueNewVertices;
+				}
+
+				assert(piRemapTable[piVertexIDs[l]] == -1);	// has not yet been assigned
+				piRemapTable[piVertexIDs[l]] = piCurNrUniqueVertices[0] + l2;
+			}
+
+			piCurNrUniqueVertices[0] += iUniqueNewVertices;
+		}
+		else
+		{
+			MergeVertsFast(piCurNrUniqueVertices, piRemapTable, pfVertexDataOut, piVertexIDs,
+						   pfVertexDataIn, iNrVerticesIn, iFloatsPerVert,
+							iL_in, iR_in, iChannelNum+1);
+		}
+	}
+	else
+	{
+		int iL=iL_in, iR=iR_in, index;
+
+		// seperate (by fSep) all points between iL_in and iR_in in pTmpVert[]
+		while(iL < iR)
+		{
+			int iReadyLeftSwap = 0;
+			int iReadyRightSwap = 0;
+			while(iReadyLeftSwap==0 && iL<iR)
+			{
+				assert(iL>=iL_in && iL<=iR_in);
+				index = piVertexIDs[iL]*iFloatsPerVert+iChannelNum;
+				iReadyLeftSwap = !(pfVertexDataIn[index]<fAvg) ? 1 : 0;
+				if(iReadyLeftSwap==0) ++iL;
+			}
+			while(iReadyRightSwap==0 && iL<iR)
+			{
+				assert(iR>=iL_in && iR<=iR_in);
+				index = piVertexIDs[iR]*iFloatsPerVert+iChannelNum;
+				iReadyRightSwap = pfVertexDataIn[index]<fAvg ? 1 : 0;
+				if(iReadyRightSwap==0) --iR;
+			}
+			assert( (iL<iR) || (iReadyLeftSwap==0 || iReadyRightSwap==0));
+
+			if(iReadyLeftSwap!=0 && iReadyRightSwap!=0)
+			{
+				int iID=0;
+				assert(iL<iR);
+				iID = piVertexIDs[iL];
+				piVertexIDs[iL] = piVertexIDs[iR];
+				piVertexIDs[iR] = iID;
+				++iL; --iR;
+			}
+		}
+
+		assert(iL==(iR+1) || (iL==iR));
+		if(iL==iR)
+		{
+			const int index = piVertexIDs[iR]*iFloatsPerVert+iChannelNum;
+			const int iReadyRightSwap = pfVertexDataIn[index]<fAvg ? 1 : 0;
+			if(iReadyRightSwap!=0) ++iL;
+			else --iR;
+		}
+
+		// recurse
+		if(iL_in <= iR)
+			MergeVertsFast(piCurNrUniqueVertices, piRemapTable, pfVertexDataOut, piVertexIDs,
+						   pfVertexDataIn, iNrVerticesIn, iFloatsPerVert, iL_in, iR, iChannelNum);	// weld all left of fSep
+		if(iL <= iR_in)
+			MergeVertsFast(piCurNrUniqueVertices, piRemapTable, pfVertexDataOut, piVertexIDs,
+						   pfVertexDataIn, iNrVerticesIn, iFloatsPerVert, iL, iR_in, iChannelNum);	// weld all right of (or equal to) fSep
+	}
+}
\ No newline at end of file
Index: source/graphics/TerrainTextureEntry.h
===================================================================
--- source/graphics/TerrainTextureEntry.h	(revision 11919)
+++ source/graphics/TerrainTextureEntry.h	(working copy)
@@ -47,6 +47,8 @@
 	CTerrainPropertiesPtr m_pProperties;
 	
 	CTexturePtr m_Texture;
+	CTexturePtr m_Normal;
+	CTexturePtr m_Specular;
 
 	CMatrix3D m_TextureMatrix;
 	
@@ -79,6 +81,14 @@
 		return m_Texture;
 	}
 
+	const CTexturePtr& GetNormal() {
+		return m_Normal;
+	}
+
+	const CTexturePtr& GetSpecular() {
+		return m_Specular;
+	}
+
 	// Returns a matrix of the form [c 0 -s 0; -s 0 -c 0; 0 0 0 0; 0 0 0 1]
 	// mapping world-space (x,y,z,1) coordinates onto (u,v,0,1) texcoords
 	const float* GetTextureMatrix();
Index: source/graphics/mikktspace.cpp
===================================================================
--- source/graphics/mikktspace.cpp	(revision 0)
+++ source/graphics/mikktspace.cpp	(revision 0)
@@ -0,0 +1,1896 @@
+#include "precompiled.h"
+
+/** \file mikktspace/mikktspace.c
+ *  \ingroup mikktspace
+ */
+/**
+ *  Copyright (C) 2011 by Morten S. Mikkelsen
+ *
+ *  This software is provided 'as-is', without any express or implied
+ *  warranty.  In no event will the authors be held liable for any damages
+ *  arising from the use of this software.
+ *
+ *  Permission is granted to anyone to use this software for any purpose,
+ *  including commercial applications, and to alter it and redistribute it
+ *  freely, subject to the following restrictions:
+ *
+ *  1. The origin of this software must not be misrepresented; you must not
+ *     claim that you wrote the original software. If you use this software
+ *     in a product, an acknowledgment in the product documentation would be
+ *     appreciated but is not required.
+ *  2. Altered source versions must be plainly marked as such, and must not be
+ *     misrepresented as being the original software.
+ *  3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <float.h>
+#include <stdlib.h>
+
+#include "mikktspace.h"
+
+#define TFALSE		0
+#define TTRUE		1
+
+#ifndef M_PI
+#define M_PI	3.1415926535897932384626433832795
+#endif
+
+#define INTERNAL_RND_SORT_SEED		39871946
+
+// internal structure
+typedef struct
+{
+	float x, y, z;
+} SVec3;
+
+static tbool			veq( const SVec3 v1, const SVec3 v2 )
+{
+	return (v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z);
+}
+
+static SVec3		vadd( const SVec3 v1, const SVec3 v2 )
+{
+	SVec3 vRes;
+
+	vRes.x = v1.x + v2.x;
+	vRes.y = v1.y + v2.y;
+	vRes.z = v1.z + v2.z;
+
+	return vRes;
+}
+
+
+static SVec3		vsub( const SVec3 v1, const SVec3 v2 )
+{
+	SVec3 vRes;
+
+	vRes.x = v1.x - v2.x;
+	vRes.y = v1.y - v2.y;
+	vRes.z = v1.z - v2.z;
+
+	return vRes;
+}
+
+static SVec3		vscale(const float fS, const SVec3 v)
+{
+	SVec3 vRes;
+
+	vRes.x = fS * v.x;
+	vRes.y = fS * v.y;
+	vRes.z = fS * v.z;
+
+	return vRes;
+}
+
+static float			LengthSquared( const SVec3 v )
+{
+	return v.x*v.x + v.y*v.y + v.z*v.z;
+}
+
+static float			Length( const SVec3 v )
+{
+	return sqrtf(LengthSquared(v));
+}
+
+static SVec3		Normalize( const SVec3 v )
+{
+	return vscale(1 / Length(v), v);
+}
+
+static float		vdot( const SVec3 v1, const SVec3 v2)
+{
+	return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
+}
+
+
+static tbool NotZero(const float fX)
+{
+	// could possibly use FLT_EPSILON instead
+	return fabsf(fX) > FLT_MIN;
+}
+
+static tbool VNotZero(const SVec3 v)
+{
+	// might change this to an epsilon based test
+	return NotZero(v.x) || NotZero(v.y) || NotZero(v.z);
+}
+
+
+
+typedef struct
+{
+	int iNrFaces;
+	int * pTriMembers;
+} SSubGroup;
+
+typedef struct
+{
+	int iNrFaces;
+	int * pFaceIndices;
+	int iVertexRepresentitive;
+	tbool bOrientPreservering;
+} SGroup;
+
+// 
+#define MARK_DEGENERATE				1
+#define QUAD_ONE_DEGEN_TRI			2
+#define GROUP_WITH_ANY				4
+#define ORIENT_PRESERVING			8
+
+
+
+typedef struct
+{
+	int FaceNeighbors[3];
+	SGroup * AssignedGroup[3];
+	
+	// normalized first order face derivatives
+	SVec3 vOs, vOt;
+	float fMagS, fMagT;	// original magnitudes
+
+	// determines if the current and the next triangle are a quad.
+	int iOrgFaceNumber;
+	int iFlag, iTSpacesOffs;
+	unsigned char vert_num[4];
+} STriInfo;
+
+typedef struct
+{
+	SVec3 vOs;
+	float fMagS;
+	SVec3 vOt;
+	float fMagT;
+	int iCounter;	// this is to average back into quads.
+	tbool bOrient;
+} STSpace;
+
+static int GenerateInitialVerticesIndexList(STriInfo pTriInfos[], int piTriList_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn);
+static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn);
+static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn);
+static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn);
+static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[],
+                             const int iNrActiveGroups, const int piTriListIn[], const float fThresCos,
+                             const SMikkTSpaceContext * pContext);
+
+static int MakeIndex(const int iFace, const int iVert)
+{
+	assert(iVert>=0 && iVert<4 && iFace>=0);
+	return (iFace<<2) | (iVert&0x3);
+}
+
+static void IndexToData(int * piFace, int * piVert, const int iIndexIn)
+{
+	piVert[0] = iIndexIn&0x3;
+	piFace[0] = iIndexIn>>2;
+}
+
+static STSpace AvgTSpace(const STSpace * pTS0, const STSpace * pTS1)
+{
+	STSpace ts_res;
+
+	// this if is important. Due to floating point precision
+	// averaging when ts0==ts1 will cause a slight difference
+	// which results in tangent space splits later on
+	if (pTS0->fMagS==pTS1->fMagS && pTS0->fMagT==pTS1->fMagT &&
+	   veq(pTS0->vOs,pTS1->vOs)	&& veq(pTS0->vOt, pTS1->vOt))
+	{
+		ts_res.fMagS = pTS0->fMagS;
+		ts_res.fMagT = pTS0->fMagT;
+		ts_res.vOs = pTS0->vOs;
+		ts_res.vOt = pTS0->vOt;
+	}
+	else
+	{
+		ts_res.fMagS = 0.5f*(pTS0->fMagS+pTS1->fMagS);
+		ts_res.fMagT = 0.5f*(pTS0->fMagT+pTS1->fMagT);
+		ts_res.vOs = vadd(pTS0->vOs,pTS1->vOs);
+		ts_res.vOt = vadd(pTS0->vOt,pTS1->vOt);
+		if ( VNotZero(ts_res.vOs) ) ts_res.vOs = Normalize(ts_res.vOs);
+		if ( VNotZero(ts_res.vOt) ) ts_res.vOt = Normalize(ts_res.vOt);
+	}
+
+	return ts_res;
+}
+
+
+
+static SVec3 GetPosition(const SMikkTSpaceContext * pContext, const int index);
+static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index);
+static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index);
+
+
+// degen triangles
+static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris);
+static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris);
+
+
+tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext)
+{
+	return genTangSpace(pContext, 180.0f);
+}
+
+tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold)
+{
+	// count nr_triangles
+	int * piTriListIn = NULL, * piGroupTrianglesBuffer = NULL;
+	STriInfo * pTriInfos = NULL;
+	SGroup * pGroups = NULL;
+	STSpace * psTspace = NULL;
+	int iNrTrianglesIn = 0, f=0, t=0, i=0;
+	int iNrTSPaces = 0, iTotTris = 0, iDegenTriangles = 0, iNrMaxGroups = 0;
+	int iNrActiveGroups = 0, index = 0;
+	const int iNrFaces = pContext->m_pInterface->m_getNumFaces(pContext);
+	tbool bRes = TFALSE;
+	const float fThresCos = (float) cos((fAngularThreshold*(float)M_PI)/180.0f);
+
+	// verify all call-backs have been set
+	if ( pContext->m_pInterface->m_getNumFaces==NULL ||
+		pContext->m_pInterface->m_getNumVerticesOfFace==NULL ||
+		pContext->m_pInterface->m_getPosition==NULL ||
+		pContext->m_pInterface->m_getNormal==NULL ||
+		pContext->m_pInterface->m_getTexCoord==NULL )
+		return TFALSE;
+
+	// count triangles on supported faces
+	for (f=0; f<iNrFaces; f++)
+	{
+		const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f);
+		if (verts==3) ++iNrTrianglesIn;
+		else if(verts==4) iNrTrianglesIn += 2;
+	}
+	if (iNrTrianglesIn<=0) return TFALSE;
+
+	// allocate memory for an index list
+	piTriListIn = (int *) malloc(sizeof(int)*3*iNrTrianglesIn);
+	pTriInfos = (STriInfo *) malloc(sizeof(STriInfo)*iNrTrianglesIn);
+	if (piTriListIn==NULL || pTriInfos==NULL)
+	{
+		if (piTriListIn!=NULL) free(piTriListIn);
+		if (pTriInfos!=NULL) free(pTriInfos);
+		return TFALSE;
+	}
+
+	// make an initial triangle --> face index list
+	iNrTSPaces = GenerateInitialVerticesIndexList(pTriInfos, piTriListIn, pContext, iNrTrianglesIn);
+
+	// make a welded index list of identical positions and attributes (pos, norm, texc)
+	//printf("gen welded index list begin\n");
+	GenerateSharedVerticesIndexList(piTriListIn, pContext, iNrTrianglesIn);
+	//printf("gen welded index list end\n");
+
+	// Mark all degenerate triangles
+	iTotTris = iNrTrianglesIn;
+	iDegenTriangles = 0;
+	for (t=0; t<iTotTris; t++)
+	{
+		const int i0 = piTriListIn[t*3+0];
+		const int i1 = piTriListIn[t*3+1];
+		const int i2 = piTriListIn[t*3+2];
+		const SVec3 p0 = GetPosition(pContext, i0);
+		const SVec3 p1 = GetPosition(pContext, i1);
+		const SVec3 p2 = GetPosition(pContext, i2);
+		if (veq(p0,p1) || veq(p0,p2) || veq(p1,p2))	// degenerate
+		{
+			pTriInfos[t].iFlag |= MARK_DEGENERATE;
+			++iDegenTriangles;
+		}
+	}
+	iNrTrianglesIn = iTotTris - iDegenTriangles;
+
+	// mark all triangle pairs that belong to a quad with only one
+	// good triangle. These need special treatment in DegenEpilogue().
+	// Additionally, move all good triangles to the start of
+	// pTriInfos[] and piTriListIn[] without changing order and
+	// put the degenerate triangles last.
+	DegenPrologue(pTriInfos, piTriListIn, iNrTrianglesIn, iTotTris);
+
+	
+	// evaluate triangle level attributes and neighbor list
+	//printf("gen neighbors list begin\n");
+	InitTriInfo(pTriInfos, piTriListIn, pContext, iNrTrianglesIn);
+	//printf("gen neighbors list end\n");
+
+	
+	// based on the 4 rules, identify groups based on connectivity
+	iNrMaxGroups = iNrTrianglesIn*3;
+	pGroups = (SGroup *) malloc(sizeof(SGroup)*iNrMaxGroups);
+	piGroupTrianglesBuffer = (int *) malloc(sizeof(int)*iNrTrianglesIn*3);
+	if (pGroups==NULL || piGroupTrianglesBuffer==NULL)
+	{
+		if (pGroups!=NULL) free(pGroups);
+		if (piGroupTrianglesBuffer!=NULL) free(piGroupTrianglesBuffer);
+		free(piTriListIn);
+		free(pTriInfos);
+		return TFALSE;
+	}
+	//printf("gen 4rule groups begin\n");
+	iNrActiveGroups =
+		Build4RuleGroups(pTriInfos, pGroups, piGroupTrianglesBuffer, piTriListIn, iNrTrianglesIn);
+	//printf("gen 4rule groups end\n");
+
+	//
+
+	psTspace = (STSpace *) malloc(sizeof(STSpace)*iNrTSPaces);
+	if (psTspace==NULL)
+	{
+		free(piTriListIn);
+		free(pTriInfos);
+		free(pGroups);
+		free(piGroupTrianglesBuffer);
+		return TFALSE;
+	}
+	memset(psTspace, 0, sizeof(STSpace)*iNrTSPaces);
+	for (t=0; t<iNrTSPaces; t++)
+	{
+		psTspace[t].vOs.x=1.0f; psTspace[t].vOs.y=0.0f; psTspace[t].vOs.z=0.0f; psTspace[t].fMagS = 1.0f;
+		psTspace[t].vOt.x=0.0f; psTspace[t].vOt.y=1.0f; psTspace[t].vOt.z=0.0f; psTspace[t].fMagT = 1.0f;
+	}
+
+	// make tspaces, each group is split up into subgroups if necessary
+	// based on fAngularThreshold. Finally a tangent space is made for
+	// every resulting subgroup
+	//printf("gen tspaces begin\n");
+	bRes = GenerateTSpaces(psTspace, pTriInfos, pGroups, iNrActiveGroups, piTriListIn, fThresCos, pContext);
+	//printf("gen tspaces end\n");
+	
+	// clean up
+	free(pGroups);
+	free(piGroupTrianglesBuffer);
+
+	if (!bRes)	// if an allocation in GenerateTSpaces() failed
+	{
+		// clean up and return false
+		free(pTriInfos); free(piTriListIn); free(psTspace);
+		return TFALSE;
+	}
+
+
+	// degenerate quads with one good triangle will be fixed by copying a space from
+	// the good triangle to the coinciding vertex.
+	// all other degenerate triangles will just copy a space from any good triangle
+	// with the same welded index in piTriListIn[].
+	DegenEpilogue(psTspace, pTriInfos, piTriListIn, pContext, iNrTrianglesIn, iTotTris);
+
+	free(pTriInfos); free(piTriListIn);
+
+	index = 0;
+	for (f=0; f<iNrFaces; f++)
+	{
+		const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f);
+		if (verts!=3 && verts!=4) continue;
+		
+
+		// I've decided to let degenerate triangles and group-with-anythings
+		// vary between left/right hand coordinate systems at the vertices.
+		// All healthy triangles on the other hand are built to always be either or.
+
+		/*// force the coordinate system orientation to be uniform for every face.
+		// (this is already the case for good triangles but not for
+		// degenerate ones and those with bGroupWithAnything==true)
+		bool bOrient = psTspace[index].bOrient;
+		if (psTspace[index].iCounter == 0)	// tspace was not derived from a group
+		{
+			// look for a space created in GenerateTSpaces() by iCounter>0
+			bool bNotFound = true;
+			int i=1;
+			while (i<verts && bNotFound)
+			{
+				if (psTspace[index+i].iCounter > 0) bNotFound=false;
+				else ++i;
+			}
+			if (!bNotFound) bOrient = psTspace[index+i].bOrient;
+		}*/
+
+		// set data
+		for (i=0; i<verts; i++)
+		{
+			const STSpace * pTSpace = &psTspace[index];
+			float tang[] = {pTSpace->vOs.x, pTSpace->vOs.y, pTSpace->vOs.z};
+			float bitang[] = {pTSpace->vOt.x, pTSpace->vOt.y, pTSpace->vOt.z};
+			if (pContext->m_pInterface->m_setTSpace!=NULL)
+				pContext->m_pInterface->m_setTSpace(pContext, tang, bitang, pTSpace->fMagS, pTSpace->fMagT, pTSpace->bOrient, f, i);
+			if (pContext->m_pInterface->m_setTSpaceBasic!=NULL)
+				pContext->m_pInterface->m_setTSpaceBasic(pContext, tang, pTSpace->bOrient==TTRUE ? 1.0f : (-1.0f), f, i);
+
+			++index;
+		}
+	}
+
+	free(psTspace);
+
+	
+	return TTRUE;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+typedef struct
+{
+	float vert[3];
+	int index;
+} STmpVert;
+
+const int g_iCells = 2048;
+
+#ifdef _MSC_VER
+	#define NOINLINE __declspec(noinline)
+#else
+	#define NOINLINE __attribute__ ((noinline))
+#endif
+
+// it is IMPORTANT that this function is called to evaluate the hash since
+// inlining could potentially reorder instructions and generate different
+// results for the same effective input value fVal.
+NOINLINE int FindGridCell(const float fMin, const float fMax, const float fVal)
+{
+	const float fIndex = g_iCells * ((fVal-fMin)/(fMax-fMin));
+	const int iIndex = fIndex<0?0:((int)fIndex);
+	return iIndex<g_iCells?iIndex:(g_iCells-1);
+}
+
+static void MergeVertsFast(int piTriList_in_and_out[], STmpVert pTmpVert[], const SMikkTSpaceContext * pContext, const int iL_in, const int iR_in);
+static void MergeVertsSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int pTable[], const int iEntries);
+static void GenerateSharedVerticesIndexListSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn);
+
+static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn)
+{
+
+	// Generate bounding box
+	int * piHashTable=NULL, * piHashCount=NULL, * piHashOffsets=NULL, * piHashCount2=NULL;
+	STmpVert * pTmpVert = NULL;
+	int i=0, iChannel=0, k=0, e=0;
+	int iMaxCount=0;
+	SVec3 vMin = GetPosition(pContext, 0), vMax = vMin, vDim;
+	float fMin, fMax;
+	for (i=1; i<(iNrTrianglesIn*3); i++)
+	{
+		const int index = piTriList_in_and_out[i];
+
+		const SVec3 vP = GetPosition(pContext, index);
+		if (vMin.x > vP.x) vMin.x = vP.x;
+		else if(vMax.x < vP.x) vMax.x = vP.x;
+		if (vMin.y > vP.y) vMin.y = vP.y;
+		else if(vMax.y < vP.y) vMax.y = vP.y;
+		if (vMin.z > vP.z) vMin.z = vP.z;
+		else if(vMax.z < vP.z) vMax.z = vP.z;
+	}
+
+	vDim = vsub(vMax,vMin);
+	iChannel = 0;
+	fMin = vMin.x; fMax=vMax.x;
+	if (vDim.y>vDim.x && vDim.y>vDim.z)
+	{
+		iChannel=1;
+		fMin = vMin.y, fMax=vMax.y;
+	}
+	else if(vDim.z>vDim.x)
+	{
+		iChannel=2;
+		fMin = vMin.z, fMax=vMax.z;
+	}
+
+	// make allocations
+	piHashTable = (int *) malloc(sizeof(int)*iNrTrianglesIn*3);
+	piHashCount = (int *) malloc(sizeof(int)*g_iCells);
+	piHashOffsets = (int *) malloc(sizeof(int)*g_iCells);
+	piHashCount2 = (int *) malloc(sizeof(int)*g_iCells);
+
+	if (piHashTable==NULL || piHashCount==NULL || piHashOffsets==NULL || piHashCount2==NULL)
+	{
+		if (piHashTable!=NULL) free(piHashTable);
+		if (piHashCount!=NULL) free(piHashCount);
+		if (piHashOffsets!=NULL) free(piHashOffsets);
+		if (piHashCount2!=NULL) free(piHashCount2);
+		GenerateSharedVerticesIndexListSlow(piTriList_in_and_out, pContext, iNrTrianglesIn);
+		return;
+	}
+	memset(piHashCount, 0, sizeof(int)*g_iCells);
+	memset(piHashCount2, 0, sizeof(int)*g_iCells);
+
+	// count amount of elements in each cell unit
+	for (i=0; i<(iNrTrianglesIn*3); i++)
+	{
+		const int index = piTriList_in_and_out[i];
+		const SVec3 vP = GetPosition(pContext, index);
+		const float fVal = iChannel==0 ? vP.x : (iChannel==1 ? vP.y : vP.z);
+		const int iCell = FindGridCell(fMin, fMax, fVal);
+		++piHashCount[iCell];
+	}
+
+	// evaluate start index of each cell.
+	piHashOffsets[0]=0;
+	for (k=1; k<g_iCells; k++)
+		piHashOffsets[k]=piHashOffsets[k-1]+piHashCount[k-1];
+
+	// insert vertices
+	for (i=0; i<(iNrTrianglesIn*3); i++)
+	{
+		const int index = piTriList_in_and_out[i];
+		const SVec3 vP = GetPosition(pContext, index);
+		const float fVal = iChannel==0 ? vP.x : (iChannel==1 ? vP.y : vP.z);
+		const int iCell = FindGridCell(fMin, fMax, fVal);
+		int * pTable = NULL;
+
+		assert(piHashCount2[iCell]<piHashCount[iCell]);
+		pTable = &piHashTable[piHashOffsets[iCell]];
+		pTable[piHashCount2[iCell]] = i;	// vertex i has been inserted.
+		++piHashCount2[iCell];
+	}
+	for (k=0; k<g_iCells; k++)
+		assert(piHashCount2[k] == piHashCount[k]);	// verify the count
+	free(piHashCount2);
+
+	// find maximum amount of entries in any hash entry
+	iMaxCount = piHashCount[0];
+	for (k=1; k<g_iCells; k++)
+		if (iMaxCount<piHashCount[k])
+			iMaxCount=piHashCount[k];
+	pTmpVert = (STmpVert *) malloc(sizeof(STmpVert)*iMaxCount);
+	
+
+	// complete the merge
+	for (k=0; k<g_iCells; k++)
+	{
+		// extract table of cell k and amount of entries in it
+		int * pTable = &piHashTable[piHashOffsets[k]];
+		const int iEntries = piHashCount[k];
+		if (iEntries < 2) continue;
+
+		if (pTmpVert!=NULL)
+		{
+			for (e=0; e<iEntries; e++)
+			{
+				int i = pTable[e];
+				const SVec3 vP = GetPosition(pContext, piTriList_in_and_out[i]);
+				pTmpVert[e].vert[0] = vP.x; pTmpVert[e].vert[1] = vP.y;
+				pTmpVert[e].vert[2] = vP.z; pTmpVert[e].index = i;
+			}
+			MergeVertsFast(piTriList_in_and_out, pTmpVert, pContext, 0, iEntries-1);
+		}
+		else
+			MergeVertsSlow(piTriList_in_and_out, pContext, pTable, iEntries);
+	}
+
+	if (pTmpVert!=NULL) { free(pTmpVert); }
+	free(piHashTable);
+	free(piHashCount);
+	free(piHashOffsets);
+}
+
+static void MergeVertsFast(int piTriList_in_and_out[], STmpVert pTmpVert[], const SMikkTSpaceContext * pContext, const int iL_in, const int iR_in)
+{
+	// make bbox
+	int c=0, l=0, channel=0;
+	float fvMin[3], fvMax[3];
+	float dx=0, dy=0, dz=0, fSep=0;
+	for (c=0; c<3; c++)
+	{	fvMin[c]=pTmpVert[iL_in].vert[c]; fvMax[c]=fvMin[c];	}
+	for (l=(iL_in+1); l<=iR_in; l++)
+		for (c=0; c<3; c++)
+			if (fvMin[c]>pTmpVert[l].vert[c]) fvMin[c]=pTmpVert[l].vert[c];
+			else if(fvMax[c]<pTmpVert[l].vert[c]) fvMax[c]=pTmpVert[l].vert[c];
+
+	dx = fvMax[0]-fvMin[0];
+	dy = fvMax[1]-fvMin[1];
+	dz = fvMax[2]-fvMin[2];
+
+	channel = 0;
+	if (dy>dx && dy>dz) channel=1;
+	else if(dz>dx) channel=2;
+
+	fSep = 0.5f*(fvMax[channel]+fvMin[channel]);
+
+	// terminate recursion when the separation/average value
+	// is no longer strictly between fMin and fMax values.
+	if (fSep>=fvMax[channel] || fSep<=fvMin[channel])
+	{
+		// complete the weld
+		for (l=iL_in; l<=iR_in; l++)
+		{
+			int i = pTmpVert[l].index;
+			const int index = piTriList_in_and_out[i];
+			const SVec3 vP = GetPosition(pContext, index);
+			const SVec3 vN = GetNormal(pContext, index);
+			const SVec3 vT = GetTexCoord(pContext, index);
+
+			tbool bNotFound = TTRUE;
+			int l2=iL_in, i2rec=-1;
+			while (l2<l && bNotFound)
+			{
+				const int i2 = pTmpVert[l2].index;
+				const int index2 = piTriList_in_and_out[i2];
+				const SVec3 vP2 = GetPosition(pContext, index2);
+				const SVec3 vN2 = GetNormal(pContext, index2);
+				const SVec3 vT2 = GetTexCoord(pContext, index2);
+				i2rec=i2;
+
+				//if(vP==vP2 && vN==vN2 && vT==vT2)
+				if (vP.x==vP2.x && vP.y==vP2.y && vP.z==vP2.z &&
+					vN.x==vN2.x && vN.y==vN2.y && vN.z==vN2.z &&
+					vT.x==vT2.x && vT.y==vT2.y && vT.z==vT2.z)
+					bNotFound = TFALSE;
+				else
+					++l2;
+			}
+			
+			// merge if previously found
+			if (!bNotFound)
+				piTriList_in_and_out[i] = piTriList_in_and_out[i2rec];
+		}
+	}
+	else
+	{
+		int iL=iL_in, iR=iR_in;
+		assert((iR_in-iL_in)>0);	// at least 2 entries
+
+		// separate (by fSep) all points between iL_in and iR_in in pTmpVert[]
+		while (iL < iR)
+		{
+			tbool bReadyLeftSwap = TFALSE, bReadyRightSwap = TFALSE;
+			while ((!bReadyLeftSwap) && iL<iR)
+			{
+				assert(iL>=iL_in && iL<=iR_in);
+				bReadyLeftSwap = !(pTmpVert[iL].vert[channel]<fSep);
+				if (!bReadyLeftSwap) ++iL;
+			}
+			while ((!bReadyRightSwap) && iL<iR)
+			{
+				assert(iR>=iL_in && iR<=iR_in);
+				bReadyRightSwap = pTmpVert[iR].vert[channel]<fSep;
+				if (!bReadyRightSwap) --iR;
+			}
+			assert( (iL<iR) || !(bReadyLeftSwap && bReadyRightSwap) );
+
+			if (bReadyLeftSwap && bReadyRightSwap)
+			{
+				const STmpVert sTmp = pTmpVert[iL];
+				assert(iL<iR);
+				pTmpVert[iL] = pTmpVert[iR];
+				pTmpVert[iR] = sTmp;
+				++iL; --iR;
+			}
+		}
+
+		assert(iL==(iR+1) || (iL==iR));
+		if (iL==iR)
+		{
+			const tbool bReadyRightSwap = pTmpVert[iR].vert[channel]<fSep;
+			if (bReadyRightSwap) ++iL;
+			else --iR;
+		}
+
+		// only need to weld when there is more than 1 instance of the (x,y,z)
+		if (iL_in < iR)
+			MergeVertsFast(piTriList_in_and_out, pTmpVert, pContext, iL_in, iR);	// weld all left of fSep
+		if (iL < iR_in)
+			MergeVertsFast(piTriList_in_and_out, pTmpVert, pContext, iL, iR_in);	// weld all right of (or equal to) fSep
+	}
+}
+
+static void MergeVertsSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int pTable[], const int iEntries)
+{
+	// this can be optimized further using a tree structure or more hashing.
+	int e=0;
+	for (e=0; e<iEntries; e++)
+	{
+		int i = pTable[e];
+		const int index = piTriList_in_and_out[i];
+		const SVec3 vP = GetPosition(pContext, index);
+		const SVec3 vN = GetNormal(pContext, index);
+		const SVec3 vT = GetTexCoord(pContext, index);
+
+		tbool bNotFound = TTRUE;
+		int e2=0, i2rec=-1;
+		while (e2<e && bNotFound)
+		{
+			const int i2 = pTable[e2];
+			const int index2 = piTriList_in_and_out[i2];
+			const SVec3 vP2 = GetPosition(pContext, index2);
+			const SVec3 vN2 = GetNormal(pContext, index2);
+			const SVec3 vT2 = GetTexCoord(pContext, index2);
+			i2rec = i2;
+
+			if (veq(vP,vP2) && veq(vN,vN2) && veq(vT,vT2))
+				bNotFound = TFALSE;
+			else
+				++e2;
+		}
+		
+		// merge if previously found
+		if (!bNotFound)
+			piTriList_in_and_out[i] = piTriList_in_and_out[i2rec];
+	}
+}
+
+static void GenerateSharedVerticesIndexListSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn)
+{
+	int iNumUniqueVerts = 0, t=0, i=0;
+	for (t=0; t<iNrTrianglesIn; t++)
+	{
+		for (i=0; i<3; i++)
+		{
+			const int offs = t*3 + i;
+			const int index = piTriList_in_and_out[offs];
+
+			const SVec3 vP = GetPosition(pContext, index);
+			const SVec3 vN = GetNormal(pContext, index);
+			const SVec3 vT = GetTexCoord(pContext, index);
+
+			tbool bFound = TFALSE;
+			int t2=0, index2rec=-1;
+			while (!bFound && t2<=t)
+			{
+				int j=0;
+				while (!bFound && j<3)
+				{
+					const int index2 = piTriList_in_and_out[t2*3 + j];
+					const SVec3 vP2 = GetPosition(pContext, index2);
+					const SVec3 vN2 = GetNormal(pContext, index2);
+					const SVec3 vT2 = GetTexCoord(pContext, index2);
+					
+					if (veq(vP,vP2) && veq(vN,vN2) && veq(vT,vT2))
+						bFound = TTRUE;
+					else
+						++j;
+				}
+				if (!bFound) ++t2;
+			}
+
+			assert(bFound);
+			// if we found our own
+			if (index2rec == index) { ++iNumUniqueVerts; }
+
+			piTriList_in_and_out[offs] = index2rec;
+		}
+	}
+}
+
+static int GenerateInitialVerticesIndexList(STriInfo pTriInfos[], int piTriList_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn)
+{
+	int iTSpacesOffs = 0, f=0, t=0;
+	int iDstTriIndex = 0;
+	for (f=0; f<pContext->m_pInterface->m_getNumFaces(pContext); f++)
+	{
+		const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f);
+		if (verts!=3 && verts!=4) continue;
+
+		pTriInfos[iDstTriIndex].iOrgFaceNumber = f;
+		pTriInfos[iDstTriIndex].iTSpacesOffs = iTSpacesOffs;
+
+		if (verts==3)
+		{
+			unsigned char * pVerts = pTriInfos[iDstTriIndex].vert_num;
+			pVerts[0]=0; pVerts[1]=1; pVerts[2]=2;
+			piTriList_out[iDstTriIndex*3+0] = MakeIndex(f, 0);
+			piTriList_out[iDstTriIndex*3+1] = MakeIndex(f, 1);
+			piTriList_out[iDstTriIndex*3+2] = MakeIndex(f, 2);
+			++iDstTriIndex;	// next
+		}
+		else
+		{
+			{
+				pTriInfos[iDstTriIndex+1].iOrgFaceNumber = f;
+				pTriInfos[iDstTriIndex+1].iTSpacesOffs = iTSpacesOffs;
+			}
+
+			{
+				// need an order independent way to evaluate
+				// tspace on quads. This is done by splitting
+				// along the shortest diagonal.
+				const int i0 = MakeIndex(f, 0);
+				const int i1 = MakeIndex(f, 1);
+				const int i2 = MakeIndex(f, 2);
+				const int i3 = MakeIndex(f, 3);
+				const SVec3 T0 = GetTexCoord(pContext, i0);
+				const SVec3 T1 = GetTexCoord(pContext, i1);
+				const SVec3 T2 = GetTexCoord(pContext, i2);
+				const SVec3 T3 = GetTexCoord(pContext, i3);
+				const float distSQ_02 = LengthSquared(vsub(T2,T0));
+				const float distSQ_13 = LengthSquared(vsub(T3,T1));
+				tbool bQuadDiagIs_02;
+				if (distSQ_02<distSQ_13)
+					bQuadDiagIs_02 = TTRUE;
+				else if(distSQ_13<distSQ_02)
+					bQuadDiagIs_02 = TFALSE;
+				else
+				{
+					const SVec3 P0 = GetPosition(pContext, i0);
+					const SVec3 P1 = GetPosition(pContext, i1);
+					const SVec3 P2 = GetPosition(pContext, i2);
+					const SVec3 P3 = GetPosition(pContext, i3);
+					const float distSQ_02 = LengthSquared(vsub(P2,P0));
+					const float distSQ_13 = LengthSquared(vsub(P3,P1));
+
+					bQuadDiagIs_02 = distSQ_13<distSQ_02 ? TFALSE : TTRUE;
+				}
+
+				if (bQuadDiagIs_02)
+				{
+					{
+						unsigned char * pVerts_A = pTriInfos[iDstTriIndex].vert_num;
+						pVerts_A[0]=0; pVerts_A[1]=1; pVerts_A[2]=2;
+					}
+					piTriList_out[iDstTriIndex*3+0] = i0;
+					piTriList_out[iDstTriIndex*3+1] = i1;
+					piTriList_out[iDstTriIndex*3+2] = i2;
+					++iDstTriIndex;	// next
+					{
+						unsigned char * pVerts_B = pTriInfos[iDstTriIndex].vert_num;
+						pVerts_B[0]=0; pVerts_B[1]=2; pVerts_B[2]=3;
+					}
+					piTriList_out[iDstTriIndex*3+0] = i0;
+					piTriList_out[iDstTriIndex*3+1] = i2;
+					piTriList_out[iDstTriIndex*3+2] = i3;
+					++iDstTriIndex;	// next
+				}
+				else
+				{
+					{
+						unsigned char * pVerts_A = pTriInfos[iDstTriIndex].vert_num;
+						pVerts_A[0]=0; pVerts_A[1]=1; pVerts_A[2]=3;
+					}
+					piTriList_out[iDstTriIndex*3+0] = i0;
+					piTriList_out[iDstTriIndex*3+1] = i1;
+					piTriList_out[iDstTriIndex*3+2] = i3;
+					++iDstTriIndex;	// next
+					{
+						unsigned char * pVerts_B = pTriInfos[iDstTriIndex].vert_num;
+						pVerts_B[0]=1; pVerts_B[1]=2; pVerts_B[2]=3;
+					}
+					piTriList_out[iDstTriIndex*3+0] = i1;
+					piTriList_out[iDstTriIndex*3+1] = i2;
+					piTriList_out[iDstTriIndex*3+2] = i3;
+					++iDstTriIndex;	// next
+				}
+			}
+		}
+
+		iTSpacesOffs += verts;
+		assert(iDstTriIndex<=iNrTrianglesIn);
+	}
+
+	for (t=0; t<iNrTrianglesIn; t++)
+		pTriInfos[t].iFlag = 0;
+
+	// return total amount of tspaces
+	return iTSpacesOffs;
+}
+
+static SVec3 GetPosition(const SMikkTSpaceContext * pContext, const int index)
+{
+	int iF, iI;
+	SVec3 res; float pos[3];
+	IndexToData(&iF, &iI, index);
+	pContext->m_pInterface->m_getPosition(pContext, pos, iF, iI);
+	res.x=pos[0]; res.y=pos[1]; res.z=pos[2];
+	return res;
+}
+
+static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index)
+{
+	int iF, iI;
+	SVec3 res; float norm[3];
+	IndexToData(&iF, &iI, index);
+	pContext->m_pInterface->m_getNormal(pContext, norm, iF, iI);
+	res.x=norm[0]; res.y=norm[1]; res.z=norm[2];
+	return res;
+}
+
+static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index)
+{
+	int iF, iI;
+	SVec3 res; float texc[2];
+	IndexToData(&iF, &iI, index);
+	pContext->m_pInterface->m_getTexCoord(pContext, texc, iF, iI);
+	res.x=texc[0]; res.y=texc[1]; res.z=1.0f;
+	return res;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+
+typedef union
+{
+	struct
+	{
+		int i0, i1, f;
+	};
+	int array[3];
+} SEdge;
+
+static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn);
+static void BuildNeighborsSlow(STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn);
+
+// returns the texture area times 2
+static float CalcTexArea(const SMikkTSpaceContext * pContext, const int indices[])
+{
+	const SVec3 t1 = GetTexCoord(pContext, indices[0]);
+	const SVec3 t2 = GetTexCoord(pContext, indices[1]);
+	const SVec3 t3 = GetTexCoord(pContext, indices[2]);
+
+	const float t21x = t2.x-t1.x;
+	const float t21y = t2.y-t1.y;
+	const float t31x = t3.x-t1.x;
+	const float t31y = t3.y-t1.y;
+
+	const float fSignedAreaSTx2 = t21x*t31y - t21y*t31x;
+
+	return fSignedAreaSTx2<0 ? (-fSignedAreaSTx2) : fSignedAreaSTx2;
+}
+
+static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn)
+{
+	int f=0, i=0, t=0;
+	// pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before this function.
+
+	// generate neighbor info list
+	for (f=0; f<iNrTrianglesIn; f++)
+		for (i=0; i<3; i++)
+		{
+			pTriInfos[f].FaceNeighbors[i] = -1;
+			pTriInfos[f].AssignedGroup[i] = NULL;
+
+			pTriInfos[f].vOs.x=0.0f; pTriInfos[f].vOs.y=0.0f; pTriInfos[f].vOs.z=0.0f;
+			pTriInfos[f].vOt.x=0.0f; pTriInfos[f].vOt.y=0.0f; pTriInfos[f].vOt.z=0.0f;
+			pTriInfos[f].fMagS = 0;
+			pTriInfos[f].fMagT = 0;
+
+			// assumed bad
+			pTriInfos[f].iFlag |= GROUP_WITH_ANY;
+		}
+
+	// evaluate first order derivatives
+	for (f=0; f<iNrTrianglesIn; f++)
+	{
+		// initial values
+		const SVec3 v1 = GetPosition(pContext, piTriListIn[f*3+0]);
+		const SVec3 v2 = GetPosition(pContext, piTriListIn[f*3+1]);
+		const SVec3 v3 = GetPosition(pContext, piTriListIn[f*3+2]);
+		const SVec3 t1 = GetTexCoord(pContext, piTriListIn[f*3+0]);
+		const SVec3 t2 = GetTexCoord(pContext, piTriListIn[f*3+1]);
+		const SVec3 t3 = GetTexCoord(pContext, piTriListIn[f*3+2]);
+
+		const float t21x = t2.x-t1.x;
+		const float t21y = t2.y-t1.y;
+		const float t31x = t3.x-t1.x;
+		const float t31y = t3.y-t1.y;
+		const SVec3 d1 = vsub(v2,v1);
+		const SVec3 d2 = vsub(v3,v1);
+
+		const float fSignedAreaSTx2 = t21x*t31y - t21y*t31x;
+		//assert(fSignedAreaSTx2!=0);
+		SVec3 vOs = vsub(vscale(t31y,d1), vscale(t21y,d2));	// eq 18
+		SVec3 vOt = vadd(vscale(-t31x,d1), vscale(t21x,d2)); // eq 19
+
+		pTriInfos[f].iFlag |= (fSignedAreaSTx2>0 ? ORIENT_PRESERVING : 0);
+
+		if ( NotZero(fSignedAreaSTx2) )
+		{
+			const float fAbsArea = fabsf(fSignedAreaSTx2);
+			const float fLenOs = Length(vOs);
+			const float fLenOt = Length(vOt);
+			const float fS = (pTriInfos[f].iFlag&ORIENT_PRESERVING)==0 ? (-1.0f) : 1.0f;
+			if ( NotZero(fLenOs) ) pTriInfos[f].vOs = vscale(fS/fLenOs, vOs);
+			if ( NotZero(fLenOt) ) pTriInfos[f].vOt = vscale(fS/fLenOt, vOt);
+
+			// evaluate magnitudes prior to normalization of vOs and vOt
+			pTriInfos[f].fMagS = fLenOs / fAbsArea;
+			pTriInfos[f].fMagT = fLenOt / fAbsArea;
+
+			// if this is a good triangle
+			if ( NotZero(pTriInfos[f].fMagS) && NotZero(pTriInfos[f].fMagT))
+				pTriInfos[f].iFlag &= (~GROUP_WITH_ANY);
+		}
+	}
+
+	// force otherwise healthy quads to a fixed orientation
+	while (t<(iNrTrianglesIn-1))
+	{
+		const int iFO_a = pTriInfos[t].iOrgFaceNumber;
+		const int iFO_b = pTriInfos[t+1].iOrgFaceNumber;
+		if (iFO_a==iFO_b)	// this is a quad
+		{
+			const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE;
+			const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE;
+			
+			// bad triangles should already have been removed by
+			// DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false
+			if ((bIsDeg_a||bIsDeg_b)==TFALSE)
+			{
+				const tbool bOrientA = (pTriInfos[t].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE;
+				const tbool bOrientB = (pTriInfos[t+1].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE;
+				// if this happens the quad has extremely bad mapping!!
+				if (bOrientA!=bOrientB)
+				{
+					//printf("found quad with bad mapping\n");
+					tbool bChooseOrientFirstTri = TFALSE;
+					if ((pTriInfos[t+1].iFlag&GROUP_WITH_ANY)!=0) bChooseOrientFirstTri = TTRUE;
+					else if( CalcTexArea(pContext, &piTriListIn[t*3+0]) >= CalcTexArea(pContext, &piTriListIn[(t+1)*3+0]) )
+						bChooseOrientFirstTri = TTRUE;
+
+					// force match
+					{
+						const int t0 = bChooseOrientFirstTri ? t : (t+1);
+						const int t1 = bChooseOrientFirstTri ? (t+1) : t;
+						pTriInfos[t1].iFlag &= (~ORIENT_PRESERVING);	// clear first
+						pTriInfos[t1].iFlag |= (pTriInfos[t0].iFlag&ORIENT_PRESERVING);	// copy bit
+					}
+				}
+			}
+			t += 2;
+		}
+		else
+			++t;
+	}
+	
+	// match up edge pairs
+	{
+		SEdge * pEdges = (SEdge *) malloc(sizeof(SEdge)*iNrTrianglesIn*3);
+		if (pEdges==NULL)
+			BuildNeighborsSlow(pTriInfos, piTriListIn, iNrTrianglesIn);
+		else
+		{
+			BuildNeighborsFast(pTriInfos, pEdges, piTriListIn, iNrTrianglesIn);
+	
+			free(pEdges);
+		}
+	}
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], const int iMyTriIndex, SGroup * pGroup);
+static void AddTriToGroup(SGroup * pGroup, const int iTriIndex);
+
+static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn)
+{
+	const int iNrMaxGroups = iNrTrianglesIn*3;
+	int iNrActiveGroups = 0;
+	int iOffset = 0, f=0, i=0;
+	for (f=0; f<iNrTrianglesIn; f++)
+	{
+		for (i=0; i<3; i++)
+		{
+			// if not assigned to a group
+			if ((pTriInfos[f].iFlag&GROUP_WITH_ANY)==0 && pTriInfos[f].AssignedGroup[i]==NULL)
+			{
+				tbool bOrPre;
+				int neigh_indexL, neigh_indexR;
+				const int vert_index = piTriListIn[f*3+i];
+				assert(iNrActiveGroups<iNrMaxGroups);
+				pTriInfos[f].AssignedGroup[i] = &pGroups[iNrActiveGroups];
+				pTriInfos[f].AssignedGroup[i]->iVertexRepresentitive = vert_index;
+				pTriInfos[f].AssignedGroup[i]->bOrientPreservering = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0;
+				pTriInfos[f].AssignedGroup[i]->iNrFaces = 0;
+				pTriInfos[f].AssignedGroup[i]->pFaceIndices = &piGroupTrianglesBuffer[iOffset];
+				++iNrActiveGroups;
+
+				AddTriToGroup(pTriInfos[f].AssignedGroup[i], f);
+				bOrPre = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE;
+				neigh_indexL = pTriInfos[f].FaceNeighbors[i];
+				neigh_indexR = pTriInfos[f].FaceNeighbors[i>0?(i-1):2];
+				if (neigh_indexL>=0) // neighbor
+				{
+					const tbool bAnswer =
+						AssignRecur(piTriListIn, pTriInfos, neigh_indexL,
+									pTriInfos[f].AssignedGroup[i] );
+					
+					const tbool bOrPre2 = (pTriInfos[neigh_indexL].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE;
+					const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE;
+					assert(bAnswer || bDiff);
+				}
+				if (neigh_indexR>=0) // neighbor
+				{
+					const tbool bAnswer =
+						AssignRecur(piTriListIn, pTriInfos, neigh_indexR,
+									pTriInfos[f].AssignedGroup[i] );
+
+					const tbool bOrPre2 = (pTriInfos[neigh_indexR].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE;
+					const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE;
+					assert(bAnswer || bDiff);
+				}
+
+				// update offset
+				iOffset += pTriInfos[f].AssignedGroup[i]->iNrFaces;
+				// since the groups are disjoint a triangle can never
+				// belong to more than 3 groups. Subsequently something
+				// is completely screwed if this assertion ever hits.
+				assert(iOffset <= iNrMaxGroups);
+			}
+		}
+	}
+
+	return iNrActiveGroups;
+}
+
+static void AddTriToGroup(SGroup * pGroup, const int iTriIndex)
+{
+	pGroup->pFaceIndices[pGroup->iNrFaces] = iTriIndex;
+	++pGroup->iNrFaces;
+}
+
+static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[],
+				 const int iMyTriIndex, SGroup * pGroup)
+{
+	STriInfo * pMyTriInfo = &psTriInfos[iMyTriIndex];
+
+	// track down vertex
+	const int iVertRep = pGroup->iVertexRepresentitive;
+	const int * pVerts = &piTriListIn[3*iMyTriIndex+0];
+	int i=-1;
+	if (pVerts[0]==iVertRep) i=0;
+	else if(pVerts[1]==iVertRep) i=1;
+	else if(pVerts[2]==iVertRep) i=2;
+	assert(i>=0 && i<3);
+
+	// early out
+	if (pMyTriInfo->AssignedGroup[i] == pGroup) return TTRUE;
+	else if(pMyTriInfo->AssignedGroup[i]!=NULL) return TFALSE;
+	if ((pMyTriInfo->iFlag&GROUP_WITH_ANY)!=0)
+	{
+		// first to group with a group-with-anything triangle
+		// determines it's orientation.
+		// This is the only existing order dependency in the code!!
+		if ( pMyTriInfo->AssignedGroup[0] == NULL &&
+			pMyTriInfo->AssignedGroup[1] == NULL &&
+			pMyTriInfo->AssignedGroup[2] == NULL )
+		{
+			pMyTriInfo->iFlag &= (~ORIENT_PRESERVING);
+			pMyTriInfo->iFlag |= (pGroup->bOrientPreservering ? ORIENT_PRESERVING : 0);
+		}
+	}
+	{
+		const tbool bOrient = (pMyTriInfo->iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE;
+		if (bOrient != pGroup->bOrientPreservering) return TFALSE;
+	}
+
+	AddTriToGroup(pGroup, iMyTriIndex);
+	pMyTriInfo->AssignedGroup[i] = pGroup;
+
+	{
+		const int neigh_indexL = pMyTriInfo->FaceNeighbors[i];
+		const int neigh_indexR = pMyTriInfo->FaceNeighbors[i>0?(i-1):2];
+		if (neigh_indexL>=0)
+			AssignRecur(piTriListIn, psTriInfos, neigh_indexL, pGroup);
+		if (neigh_indexR>=0)
+			AssignRecur(piTriListIn, psTriInfos, neigh_indexR, pGroup);
+	}
+
+
+
+	return TTRUE;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2);
+static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed);
+static STSpace EvalTspace(int face_indices[], const int iFaces, const int piTriListIn[], const STriInfo pTriInfos[], const SMikkTSpaceContext * pContext, const int iVertexRepresentitive);
+
+static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[],
+                             const int iNrActiveGroups, const int piTriListIn[], const float fThresCos,
+                             const SMikkTSpaceContext * pContext)
+{
+	STSpace * pSubGroupTspace = NULL;
+	SSubGroup * pUniSubGroups = NULL;
+	int * pTmpMembers = NULL;
+	int iMaxNrFaces=0, iUniqueTspaces=0, g=0, i=0;
+	for (g=0; g<iNrActiveGroups; g++)
+		if (iMaxNrFaces < pGroups[g].iNrFaces)
+			iMaxNrFaces = pGroups[g].iNrFaces;
+
+	if (iMaxNrFaces == 0) return TTRUE;
+
+	// make initial allocations
+	pSubGroupTspace = (STSpace *) malloc(sizeof(STSpace)*iMaxNrFaces);
+	pUniSubGroups = (SSubGroup *) malloc(sizeof(SSubGroup)*iMaxNrFaces);
+	pTmpMembers = (int *) malloc(sizeof(int)*iMaxNrFaces);
+	if (pSubGroupTspace==NULL || pUniSubGroups==NULL || pTmpMembers==NULL)
+	{
+		if (pSubGroupTspace!=NULL) free(pSubGroupTspace);
+		if (pUniSubGroups!=NULL) free(pUniSubGroups);
+		if (pTmpMembers!=NULL) free(pTmpMembers);
+		return TFALSE;
+	}
+
+
+	iUniqueTspaces = 0;
+	for (g=0; g<iNrActiveGroups; g++)
+	{
+		const SGroup * pGroup = &pGroups[g];
+		int iUniqueSubGroups = 0, s=0;
+
+		for (i=0; i<pGroup->iNrFaces; i++)	// triangles
+		{
+			const int f = pGroup->pFaceIndices[i];	// triangle number
+			int index=-1, iVertIndex=-1, iOF_1=-1, iMembers=0, j=0, l=0;
+			SSubGroup tmp_group;
+			tbool bFound;
+			SVec3 n, vOs, vOt;
+			if (pTriInfos[f].AssignedGroup[0]==pGroup) index=0;
+			else if(pTriInfos[f].AssignedGroup[1]==pGroup) index=1;
+			else if(pTriInfos[f].AssignedGroup[2]==pGroup) index=2;
+			assert(index>=0 && index<3);
+
+			iVertIndex = piTriListIn[f*3+index];
+			assert(iVertIndex==pGroup->iVertexRepresentitive);
+
+			// is normalized already
+			n = GetNormal(pContext, iVertIndex);
+			
+			// project
+			vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n));
+			vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n));
+			if ( VNotZero(vOs) ) vOs = Normalize(vOs);
+			if ( VNotZero(vOt) ) vOt = Normalize(vOt);
+
+			// original face number
+			iOF_1 = pTriInfos[f].iOrgFaceNumber;
+			
+			iMembers = 0;
+			for (j=0; j<pGroup->iNrFaces; j++)
+			{
+				const int t = pGroup->pFaceIndices[j];	// triangle number
+				const int iOF_2 = pTriInfos[t].iOrgFaceNumber;
+
+				// project
+				SVec3 vOs2 = vsub(pTriInfos[t].vOs, vscale(vdot(n,pTriInfos[t].vOs), n));
+				SVec3 vOt2 = vsub(pTriInfos[t].vOt, vscale(vdot(n,pTriInfos[t].vOt), n));
+				if ( VNotZero(vOs2) ) vOs2 = Normalize(vOs2);
+				if ( VNotZero(vOt2) ) vOt2 = Normalize(vOt2);
+
+				{
+					const tbool bAny = ( (pTriInfos[f].iFlag | pTriInfos[t].iFlag) & GROUP_WITH_ANY )!=0 ? TTRUE : TFALSE;
+					// make sure triangles which belong to the same quad are joined.
+					const tbool bSameOrgFace = iOF_1==iOF_2 ? TTRUE : TFALSE;
+
+					const float fCosS = vdot(vOs,vOs2);
+					const float fCosT = vdot(vOt,vOt2);
+
+					assert(f!=t || bSameOrgFace);	// sanity check
+					if (bAny || bSameOrgFace || (fCosS>fThresCos && fCosT>fThresCos))
+						pTmpMembers[iMembers++] = t;
+				}
+			}
+
+			// sort pTmpMembers
+			tmp_group.iNrFaces = iMembers;
+			tmp_group.pTriMembers = pTmpMembers;
+			if (iMembers>1)
+			{
+				unsigned int uSeed = INTERNAL_RND_SORT_SEED;	// could replace with a random seed?
+				QuickSort(pTmpMembers, 0, iMembers-1, uSeed);
+			}
+
+			// look for an existing match
+			bFound = TFALSE;
+			l=0;
+			while (l<iUniqueSubGroups && !bFound)
+			{
+				bFound = CompareSubGroups(&tmp_group, &pUniSubGroups[l]);
+				if (!bFound) ++l;
+			}
+			
+			// assign tangent space index
+			assert(bFound || l==iUniqueSubGroups);
+			//piTempTangIndices[f*3+index] = iUniqueTspaces+l;
+
+			// if no match was found we allocate a new subgroup
+			if (!bFound)
+			{
+				// insert new subgroup
+				int * pIndices = (int *) malloc(sizeof(int)*iMembers);
+				if (pIndices==NULL)
+				{
+					// clean up and return false
+					int s=0;
+					for (s=0; s<iUniqueSubGroups; s++)
+						free(pUniSubGroups[s].pTriMembers);
+					free(pUniSubGroups);
+					free(pTmpMembers);
+					free(pSubGroupTspace);
+					return TFALSE;
+				}
+				pUniSubGroups[iUniqueSubGroups].iNrFaces = iMembers;
+				pUniSubGroups[iUniqueSubGroups].pTriMembers = pIndices;
+				memcpy(pIndices, tmp_group.pTriMembers, iMembers*sizeof(int));
+				pSubGroupTspace[iUniqueSubGroups] =
+					EvalTspace(tmp_group.pTriMembers, iMembers, piTriListIn, pTriInfos, pContext, pGroup->iVertexRepresentitive);
+				++iUniqueSubGroups;
+			}
+
+			// output tspace
+			{
+				const int iOffs = pTriInfos[f].iTSpacesOffs;
+				const int iVert = pTriInfos[f].vert_num[index];
+				STSpace * pTS_out = &psTspace[iOffs+iVert];
+				assert(pTS_out->iCounter<2);
+				assert(((pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0) == pGroup->bOrientPreservering);
+				if (pTS_out->iCounter==1)
+				{
+					*pTS_out = AvgTSpace(pTS_out, &pSubGroupTspace[l]);
+					pTS_out->iCounter = 2;	// update counter
+					pTS_out->bOrient = pGroup->bOrientPreservering;
+				}
+				else
+				{
+					assert(pTS_out->iCounter==0);
+					*pTS_out = pSubGroupTspace[l];
+					pTS_out->iCounter = 1;	// update counter
+					pTS_out->bOrient = pGroup->bOrientPreservering;
+				}
+			}
+		}
+
+		// clean up and offset iUniqueTspaces
+		for (s=0; s<iUniqueSubGroups; s++)
+			free(pUniSubGroups[s].pTriMembers);
+		iUniqueTspaces += iUniqueSubGroups;
+	}
+
+	// clean up
+	free(pUniSubGroups);
+	free(pTmpMembers);
+	free(pSubGroupTspace);
+
+	return TTRUE;
+}
+
+static STSpace EvalTspace(int face_indices[], const int iFaces, const int piTriListIn[], const STriInfo pTriInfos[],
+                          const SMikkTSpaceContext * pContext, const int iVertexRepresentitive)
+{
+	STSpace res;
+	float fAngleSum = 0;
+	int face=0;
+	res.vOs.x=0.0f; res.vOs.y=0.0f; res.vOs.z=0.0f;
+	res.vOt.x=0.0f; res.vOt.y=0.0f; res.vOt.z=0.0f;
+	res.fMagS = 0; res.fMagT = 0;
+
+	for (face=0; face<iFaces; face++)
+	{
+		const int f = face_indices[face];
+
+		// only valid triangles get to add their contribution
+		if ( (pTriInfos[f].iFlag&GROUP_WITH_ANY)==0 )
+		{
+			SVec3 n, vOs, vOt, p0, p1, p2, v1, v2;
+			float fCos, fAngle, fMagS, fMagT;
+			int i=-1, index=-1, i0=-1, i1=-1, i2=-1;
+			if (piTriListIn[3*f+0]==iVertexRepresentitive) i=0;
+			else if(piTriListIn[3*f+1]==iVertexRepresentitive) i=1;
+			else if(piTriListIn[3*f+2]==iVertexRepresentitive) i=2;
+			assert(i>=0 && i<3);
+
+			// project
+			index = piTriListIn[3*f+i];
+			n = GetNormal(pContext, index);
+			vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n));
+			vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n));
+			if ( VNotZero(vOs) ) vOs = Normalize(vOs);
+			if ( VNotZero(vOt) ) vOt = Normalize(vOt);
+
+			i2 = piTriListIn[3*f + (i<2?(i+1):0)];
+			i1 = piTriListIn[3*f + i];
+			i0 = piTriListIn[3*f + (i>0?(i-1):2)];
+
+			p0 = GetPosition(pContext, i0);
+			p1 = GetPosition(pContext, i1);
+			p2 = GetPosition(pContext, i2);
+			v1 = vsub(p0,p1);
+			v2 = vsub(p2,p1);
+
+			// project
+			v1 = vsub(v1, vscale(vdot(n,v1),n)); if( VNotZero(v1) ) v1 = Normalize(v1);
+			v2 = vsub(v2, vscale(vdot(n,v2),n)); if( VNotZero(v2) ) v2 = Normalize(v2);
+
+			// weight contribution by the angle
+			// between the two edge vectors
+			fCos = vdot(v1,v2); fCos=fCos>1?1:(fCos<(-1) ? (-1) : fCos);
+			fAngle = (float) acos(fCos);
+			fMagS = pTriInfos[f].fMagS;
+			fMagT = pTriInfos[f].fMagT;
+
+			res.vOs=vadd(res.vOs, vscale(fAngle,vOs));
+			res.vOt=vadd(res.vOt,vscale(fAngle,vOt));
+			res.fMagS+=(fAngle*fMagS);
+			res.fMagT+=(fAngle*fMagT);
+			fAngleSum += fAngle;
+		}
+	}
+
+	// normalize
+	if ( VNotZero(res.vOs) ) res.vOs = Normalize(res.vOs);
+	if ( VNotZero(res.vOt) ) res.vOt = Normalize(res.vOt);
+	if (fAngleSum>0)
+	{
+		res.fMagS /= fAngleSum;
+		res.fMagT /= fAngleSum;
+	}
+
+	return res;
+}
+
+static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2)
+{
+	tbool bStillSame=TTRUE;
+	int i=0;
+	if (pg1->iNrFaces!=pg2->iNrFaces) return TFALSE;
+	while (i<pg1->iNrFaces && bStillSame)
+	{
+		bStillSame = pg1->pTriMembers[i]==pg2->pTriMembers[i] ? TTRUE : TFALSE;
+		if (bStillSame) ++i;
+	}
+	return bStillSame;
+}
+
+static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed)
+{
+	int iL, iR, n, index, iMid, iTmp;
+
+	// Random
+	unsigned int t=uSeed&31;
+	t=(uSeed<<t)|(uSeed>>(32-t));
+	uSeed=uSeed+t+3;
+	// Random end
+
+	iL=iLeft; iR=iRight;
+	n = (iR-iL)+1;
+	assert(n>=0);
+	index = (int) (uSeed%n);
+
+	iMid=pSortBuffer[index + iL];
+
+
+	do
+	{
+		while (pSortBuffer[iL] < iMid)
+			++iL;
+		while (pSortBuffer[iR] > iMid)
+			--iR;
+
+		if (iL <= iR)
+		{
+			iTmp = pSortBuffer[iL];
+			pSortBuffer[iL] = pSortBuffer[iR];
+			pSortBuffer[iR] = iTmp;
+			++iL; --iR;
+		}
+	}
+	while (iL <= iR);
+
+	if (iLeft < iR)
+		QuickSort(pSortBuffer, iLeft, iR, uSeed);
+	if (iL < iRight)
+		QuickSort(pSortBuffer, iL, iRight, uSeed);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////
+
+static void QuickSortEdges(SEdge * pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed);
+static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in);
+
+static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn)
+{
+	// build array of edges
+	unsigned int uSeed = INTERNAL_RND_SORT_SEED;				// could replace with a random seed?
+	int iEntries=0, iCurStartIndex=-1, f=0, i=0;
+	for (f=0; f<iNrTrianglesIn; f++)
+		for (i=0; i<3; i++)
+		{
+			const int i0 = piTriListIn[f*3+i];
+			const int i1 = piTriListIn[f*3+(i<2?(i+1):0)];
+			pEdges[f*3+i].i0 = i0 < i1 ? i0 : i1;			// put minimum index in i0
+			pEdges[f*3+i].i1 = !(i0 < i1) ? i0 : i1;		// put maximum index in i1
+			pEdges[f*3+i].f = f;							// record face number
+		}
+
+	// sort over all edges by i0, this is the pricy one.
+	QuickSortEdges(pEdges, 0, iNrTrianglesIn*3-1, 0, uSeed);	// sort channel 0 which is i0
+
+	// sub sort over i1, should be fast.
+	// could replace this with a 64 bit int sort over (i0,i1)
+	// with i0 as msb in the quicksort call above.
+	iEntries = iNrTrianglesIn*3;
+	iCurStartIndex = 0;
+	for (i=1; i<iEntries; i++)
+	{
+		if (pEdges[iCurStartIndex].i0 != pEdges[i].i0)
+		{
+			const int iL = iCurStartIndex;
+			const int iR = i-1;
+			//const int iElems = i-iL;
+			iCurStartIndex = i;
+			QuickSortEdges(pEdges, iL, iR, 1, uSeed);	// sort channel 1 which is i1
+		}
+	}
+
+	// sub sort over f, which should be fast.
+	// this step is to remain compliant with BuildNeighborsSlow() when
+	// more than 2 triangles use the same edge (such as a butterfly topology).
+	iCurStartIndex = 0;
+	for (i=1; i<iEntries; i++)
+	{
+		if (pEdges[iCurStartIndex].i0 != pEdges[i].i0 || pEdges[iCurStartIndex].i1 != pEdges[i].i1)
+		{
+			const int iL = iCurStartIndex;
+			const int iR = i-1;
+			//const int iElems = i-iL;
+			iCurStartIndex = i;
+			QuickSortEdges(pEdges, iL, iR, 2, uSeed);	// sort channel 2 which is f
+		}
+	}
+
+	// pair up, adjacent triangles
+	for (i=0; i<iEntries; i++)
+	{
+		const int i0=pEdges[i].i0;
+		const int i1=pEdges[i].i1;
+		const int f = pEdges[i].f;
+		tbool bUnassigned_A;
+
+		int i0_A, i1_A;
+		int edgenum_A, edgenum_B=0;	// 0,1 or 2
+		GetEdge(&i0_A, &i1_A, &edgenum_A, &piTriListIn[f*3], i0, i1);	// resolve index ordering and edge_num
+		bUnassigned_A = pTriInfos[f].FaceNeighbors[edgenum_A] == -1 ? TTRUE : TFALSE;
+
+		if (bUnassigned_A)
+		{
+			// get true index ordering
+			int j=i+1, t;
+			tbool bNotFound = TTRUE;
+			while (j<iEntries && i0==pEdges[j].i0 && i1==pEdges[j].i1 && bNotFound)
+			{
+				tbool bUnassigned_B;
+				int i0_B, i1_B;
+				t = pEdges[j].f;
+				// flip i0_B and i1_B
+				GetEdge(&i1_B, &i0_B, &edgenum_B, &piTriListIn[t*3], pEdges[j].i0, pEdges[j].i1);	// resolve index ordering and edge_num
+				//assert(!(i0_A==i1_B && i1_A==i0_B));
+				bUnassigned_B =  pTriInfos[t].FaceNeighbors[edgenum_B]==-1 ? TTRUE : TFALSE;
+				if (i0_A==i0_B && i1_A==i1_B && bUnassigned_B)
+					bNotFound = TFALSE;
+				else
+					++j;
+			}
+
+			if (!bNotFound)
+			{
+				int t = pEdges[j].f;
+				pTriInfos[f].FaceNeighbors[edgenum_A] = t;
+				//assert(pTriInfos[t].FaceNeighbors[edgenum_B]==-1);
+				pTriInfos[t].FaceNeighbors[edgenum_B] = f;
+			}
+		}
+	}
+}
+
+static void BuildNeighborsSlow(STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn)
+{
+	int f=0, i=0;
+	for (f=0; f<iNrTrianglesIn; f++)
+	{
+		for (i=0; i<3; i++)
+		{
+			// if unassigned
+			if (pTriInfos[f].FaceNeighbors[i] == -1)
+			{
+				const int i0_A = piTriListIn[f*3+i];
+				const int i1_A = piTriListIn[f*3+(i<2?(i+1):0)];
+
+				// search for a neighbor
+				tbool bFound = TFALSE;
+				int t=0, j=0;
+				while (!bFound && t<iNrTrianglesIn)
+				{
+					if (t!=f)
+					{
+						j=0;
+						while (!bFound && j<3)
+						{
+							// in rev order
+							const int i1_B = piTriListIn[t*3+j];
+							const int i0_B = piTriListIn[t*3+(j<2?(j+1):0)];
+							//assert(!(i0_A==i1_B && i1_A==i0_B));
+							if (i0_A==i0_B && i1_A==i1_B)
+								bFound = TTRUE;
+							else
+								++j;
+						}
+					}
+					
+					if (!bFound) ++t;
+				}
+
+				// assign neighbors
+				if (bFound)
+				{
+					pTriInfos[f].FaceNeighbors[i] = t;
+					//assert(pTriInfos[t].FaceNeighbors[j]==-1);
+					pTriInfos[t].FaceNeighbors[j] = f;
+				}
+			}
+		}
+	}
+}
+
+static void QuickSortEdges(SEdge * pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed)
+{
+	unsigned int t;
+	int iL, iR, n, index, iMid;
+
+	// early out
+	SEdge sTmp;
+	const int iElems = iRight-iLeft+1;
+	if (iElems<2) return;
+	else if(iElems==2)
+	{
+		if (pSortBuffer[iLeft].array[channel] > pSortBuffer[iRight].array[channel])
+		{
+			sTmp = pSortBuffer[iLeft];
+			pSortBuffer[iLeft] = pSortBuffer[iRight];
+			pSortBuffer[iRight] = sTmp;
+		}
+		return;
+	}
+
+	// Random
+	t=uSeed&31;
+	t=(uSeed<<t)|(uSeed>>(32-t));
+	uSeed=uSeed+t+3;
+	// Random end
+
+	iL=iLeft, iR=iRight;
+	n = (iR-iL)+1;
+	assert(n>=0);
+	index = (int) (uSeed%n);
+
+	iMid=pSortBuffer[index + iL].array[channel];
+
+	do
+	{
+		while (pSortBuffer[iL].array[channel] < iMid)
+			++iL;
+		while (pSortBuffer[iR].array[channel] > iMid)
+			--iR;
+
+		if (iL <= iR)
+		{
+			sTmp = pSortBuffer[iL];
+			pSortBuffer[iL] = pSortBuffer[iR];
+			pSortBuffer[iR] = sTmp;
+			++iL; --iR;
+		}
+	}
+	while (iL <= iR);
+
+	if (iLeft < iR)
+		QuickSortEdges(pSortBuffer, iLeft, iR, channel, uSeed);
+	if (iL < iRight)
+		QuickSortEdges(pSortBuffer, iL, iRight, channel, uSeed);
+}
+
+// resolve ordering and edge number
+static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in)
+{
+	*edgenum_out = -1;
+	
+	// test if first index is on the edge
+	if (indices[0]==i0_in || indices[0]==i1_in)
+	{
+		// test if second index is on the edge
+		if (indices[1]==i0_in || indices[1]==i1_in)
+		{
+			edgenum_out[0]=0;	// first edge
+			i0_out[0]=indices[0];
+			i1_out[0]=indices[1];
+		}
+		else
+		{
+			edgenum_out[0]=2;	// third edge
+			i0_out[0]=indices[2];
+			i1_out[0]=indices[0];
+		}
+	}
+	else
+	{
+		// only second and third index is on the edge
+		edgenum_out[0]=1;	// second edge
+		i0_out[0]=indices[1];
+		i1_out[0]=indices[2];
+	}
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////// Degenerate triangles ////////////////////////////////////
+
+static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris)
+{
+	int iNextGoodTriangleSearchIndex=-1;
+	tbool bStillFindingGoodOnes;
+
+	// locate quads with only one good triangle
+	int t=0;
+	while (t<(iTotTris-1))
+	{
+		const int iFO_a = pTriInfos[t].iOrgFaceNumber;
+		const int iFO_b = pTriInfos[t+1].iOrgFaceNumber;
+		if (iFO_a==iFO_b)	// this is a quad
+		{
+			const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE;
+			const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE;
+			if ((bIsDeg_a^bIsDeg_b)!=0)
+			{
+				pTriInfos[t].iFlag |= QUAD_ONE_DEGEN_TRI;
+				pTriInfos[t+1].iFlag |= QUAD_ONE_DEGEN_TRI;
+			}
+			t += 2;
+		}
+		else
+			++t;
+	}
+
+	// reorder list so all degen triangles are moved to the back
+	// without reordering the good triangles
+	iNextGoodTriangleSearchIndex = 1;
+	t=0;
+	bStillFindingGoodOnes = TTRUE;
+	while (t<iNrTrianglesIn && bStillFindingGoodOnes)
+	{
+		const tbool bIsGood = (pTriInfos[t].iFlag&MARK_DEGENERATE)==0 ? TTRUE : TFALSE;
+		if (bIsGood)
+		{
+			if (iNextGoodTriangleSearchIndex < (t+2))
+				iNextGoodTriangleSearchIndex = t+2;
+		}
+		else
+		{
+			int t0, t1;
+			// search for the first good triangle.
+			tbool bJustADegenerate = TTRUE;
+			while (bJustADegenerate && iNextGoodTriangleSearchIndex<iTotTris)
+			{
+				const tbool bIsGood = (pTriInfos[iNextGoodTriangleSearchIndex].iFlag&MARK_DEGENERATE)==0 ? TTRUE : TFALSE;
+				if (bIsGood) bJustADegenerate=TFALSE;
+				else ++iNextGoodTriangleSearchIndex;
+			}
+
+			t0 = t;
+			t1 = iNextGoodTriangleSearchIndex;
+			++iNextGoodTriangleSearchIndex;
+			assert(iNextGoodTriangleSearchIndex > (t+1));
+
+			// swap triangle t0 and t1
+			if (!bJustADegenerate)
+			{
+				int i=0;
+				for (i=0; i<3; i++)
+				{
+					const int index = piTriList_out[t0*3+i];
+					piTriList_out[t0*3+i] = piTriList_out[t1*3+i];
+					piTriList_out[t1*3+i] = index;
+				}
+				{
+					const STriInfo tri_info = pTriInfos[t0];
+					pTriInfos[t0] = pTriInfos[t1];
+					pTriInfos[t1] = tri_info;
+				}
+			}
+			else
+				bStillFindingGoodOnes = TFALSE;	// this is not supposed to happen
+		}
+
+		if (bStillFindingGoodOnes) ++t;
+	}
+
+	assert(bStillFindingGoodOnes);	// code will still work.
+	assert(iNrTrianglesIn == t);
+}
+
+static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris)
+{
+	int t=0, i=0;
+	// deal with degenerate triangles
+	// punishment for degenerate triangles is O(N^2)
+	for (t=iNrTrianglesIn; t<iTotTris; t++)
+	{
+		// degenerate triangles on a quad with one good triangle are skipped
+		// here but processed in the next loop
+		const tbool bSkip = (pTriInfos[t].iFlag&QUAD_ONE_DEGEN_TRI)!=0 ? TTRUE : TFALSE;
+
+		if (!bSkip)
+		{
+			for (i=0; i<3; i++)
+			{
+				const int index1 = piTriListIn[t*3+i];
+				// search through the good triangles
+				tbool bNotFound = TTRUE;
+				int j=0;
+				while (bNotFound && j<(3*iNrTrianglesIn))
+				{
+					const int index2 = piTriListIn[j];
+					if (index1==index2) bNotFound=TFALSE;
+					else ++j;
+				}
+
+				if (!bNotFound)
+				{
+					const int iTri = j/3;
+					const int iVert = j%3;
+					const int iSrcVert=pTriInfos[iTri].vert_num[iVert];
+					const int iSrcOffs=pTriInfos[iTri].iTSpacesOffs;
+					const int iDstVert=pTriInfos[t].vert_num[i];
+					const int iDstOffs=pTriInfos[t].iTSpacesOffs;
+					
+					// copy tspace
+					psTspace[iDstOffs+iDstVert] = psTspace[iSrcOffs+iSrcVert];
+				}
+			}
+		}
+	}
+
+	// deal with degenerate quads with one good triangle
+	for (t=0; t<iNrTrianglesIn; t++)
+	{
+		// this triangle belongs to a quad where the
+		// other triangle is degenerate
+		if ( (pTriInfos[t].iFlag&QUAD_ONE_DEGEN_TRI)!=0 )
+		{
+			SVec3 vDstP;
+			int iOrgF=-1, i=0;
+			tbool bNotFound;
+			unsigned char * pV = pTriInfos[t].vert_num;
+			int iFlag = (1<<pV[0]) | (1<<pV[1]) | (1<<pV[2]);
+			int iMissingIndex = 0;
+			if ((iFlag&2)==0) iMissingIndex=1;
+			else if((iFlag&4)==0) iMissingIndex=2;
+			else if((iFlag&8)==0) iMissingIndex=3;
+
+			iOrgF = pTriInfos[t].iOrgFaceNumber;
+			vDstP = GetPosition(pContext, MakeIndex(iOrgF, iMissingIndex));
+			bNotFound = TTRUE;
+			i=0;
+			while (bNotFound && i<3)
+			{
+				const int iVert = pV[i];
+				const SVec3 vSrcP = GetPosition(pContext, MakeIndex(iOrgF, iVert));
+				if (veq(vSrcP, vDstP)==TTRUE)
+				{
+					const int iOffs = pTriInfos[t].iTSpacesOffs;
+					psTspace[iOffs+iMissingIndex] = psTspace[iOffs+iVert];
+					bNotFound=TFALSE;
+				}
+				else
+					++i;
+			}
+			assert(!bNotFound);
+		}
+	}
+}
Index: source/graphics/ObjectBase.h
===================================================================
--- source/graphics/ObjectBase.h	(revision 11919)
+++ source/graphics/ObjectBase.h	(working copy)
@@ -80,6 +80,8 @@
 		int m_Frequency;
 		VfsPath m_ModelFilename;
 		VfsPath m_TextureFilename;
+		VfsPath m_NormalFilename;
+		VfsPath m_SpecularFilename;
 		Decal m_Decal;
 		VfsPath m_Particles;
 		CStr m_Color;
@@ -91,6 +93,8 @@
 	struct Variation
 	{
 		VfsPath texture;
+		VfsPath normal;
+		VfsPath specular;
 		VfsPath model;
 		Decal decal;
 		VfsPath particles;
Index: source/graphics/TerrainTextureEntry.cpp
===================================================================
--- source/graphics/TerrainTextureEntry.cpp	(revision 11919)
+++ source/graphics/TerrainTextureEntry.cpp	(working copy)
@@ -44,7 +44,7 @@
 	// TODO: anisotropy should probably be user-configurable, but we want it to be
 	// at least 2 for terrain else the ground looks very blurry when you tilt the
 	// camera upwards
-	texture.SetMaxAnisotropy(2.0f);
+	texture.SetMaxAnisotropy(2.0f);	
 
 	if (CRenderer::IsInitialised())
 		m_Texture = g_Renderer.GetTextureManager().CreateTexture(texture);
@@ -71,6 +71,25 @@
 		(*it)->AddTerrain(this);
 	
 	m_Tag = utf8_from_wstring(path.Basename().string());
+	
+	if (m_pProperties->HasNormalMap())
+	{
+		//std::wcout << m_pProperties->GetNormalMap().string() << std::endl;
+		CTextureProperties normal(m_pProperties->GetNormalMap());
+		normal.SetWrap(GL_REPEAT);
+		normal.SetMaxAnisotropy(2.0f);
+		if (CRenderer::IsInitialised())
+			m_Normal = g_Renderer.GetTextureManager().CreateTexture(normal);
+	}
+	
+	if (m_pProperties->HasSpecularMap())
+	{
+		CTextureProperties specular(m_pProperties->GetSpecularMap());
+		specular.SetWrap(GL_REPEAT);
+		specular.SetMaxAnisotropy(2.0f);
+		if (CRenderer::IsInitialised())
+			m_Specular = g_Renderer.GetTextureManager().CreateTexture(specular);
+	}
 }
 
 CTerrainTextureEntry::~CTerrainTextureEntry()
Index: source/graphics/weldmesh.h
===================================================================
--- source/graphics/weldmesh.h	(revision 0)
+++ source/graphics/weldmesh.h	(revision 0)
@@ -0,0 +1,49 @@
+/**
+ *  Copyright (C) 2011 by Morten S. Mikkelsen
+ *
+ *  This software is provided 'as-is', without any express or implied
+ *  warranty.  In no event will the authors be held liable for any damages
+ *  arising from the use of this software.
+ *
+ *  Permission is granted to anyone to use this software for any purpose,
+ *  including commercial applications, and to alter it and redistribute it
+ *  freely, subject to the following restrictions:
+ *
+ *  1. The origin of this software must not be misrepresented; you must not
+ *     claim that you wrote the original software. If you use this software
+ *     in a product, an acknowledgment in the product documentation would be
+ *     appreciated but is not required.
+ *  2. Altered source versions must be plainly marked as such, and must not be
+ *     misrepresented as being the original software.
+ *  3. This notice may not be removed or altered from any source distribution.
+ */
+
+
+#ifndef __WELDMESH_H__
+#define __WELDMESH_H__
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// piRemapTable must be initialized and point to an area in memory
+// with the byte size: iNrVerticesIn * sizeof(int).
+// pfVertexDataOut must be initialized and point to an area in memory
+// with the byte size: iNrVerticesIn * iFloatsPerVert * sizeof(float).
+// At the end of the WeldMesh() call the array pfVertexDataOut will contain
+// unique vertices only. Each entry in piRemapTable contains the index to
+// the new location of the vertex in pfVertexDataOut in units of iFloatsPerVert.
+// Note that this code is suitable for welding both unindexed meshes but also
+// indexed meshes which need to have duplicates removed. In the latter case
+// one simply uses the remap table to convert the old index list to the new vertex array.
+// Finally, the return value is the number of unique vertices found.
+int WeldMesh(int * piRemapTable, float * pfVertexDataOut,
+			  const float pfVertexDataIn[], const int iNrVerticesIn, const int iFloatsPerVert);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif
\ No newline at end of file
Index: source/graphics/Material.cpp
===================================================================
--- source/graphics/Material.cpp	(revision 11919)
+++ source/graphics/Material.cpp	(working copy)
@@ -32,6 +32,16 @@
 	m_DiffuseTexture = texture;
 }
 
+void CMaterial::SetNormalTexture(const CTexturePtr& texture)
+{
+	m_NormalTexture = texture;
+}
+
+void CMaterial::SetSpecularTexture(const CTexturePtr& texture)
+{
+	m_SpecularTexture = texture;
+}
+
 void CMaterial::SetShaderEffect(const CStr& effect)
 {
 	m_ShaderEffect = CStrIntern(effect);
Index: source/graphics/ObjectBase.cpp
===================================================================
--- source/graphics/ObjectBase.cpp	(revision 11919)
+++ source/graphics/ObjectBase.cpp	(working copy)
@@ -62,6 +62,8 @@
 	EL(prop);
 	EL(mesh);
 	EL(texture);
+	EL(normal);
+	EL(specular);
 	EL(colour);
 	EL(decal);
 	EL(particles);
@@ -151,6 +153,14 @@
 					{
 						currentVariant->m_TextureFilename = VfsPath("art/textures/skins") / option.GetText().FromUTF8();
 					}
+					else if (option_name == el_normal)
+					{
+						currentVariant->m_NormalFilename = VfsPath("art/textures/skins") / option.GetText().FromUTF8();
+					}		
+					else if (option_name == el_specular)
+					{
+						currentVariant->m_SpecularFilename = VfsPath("art/textures/skins") / option.GetText().FromUTF8();
+					}					
 					else if (option_name == el_decal)
 					{
 						XMBAttributeList attrs = option.GetAttributes();
@@ -399,6 +409,12 @@
 		if (! var.m_TextureFilename.empty())
 			variation.texture = var.m_TextureFilename;
 
+		if (! var.m_NormalFilename.empty())
+			variation.normal = var.m_NormalFilename;
+
+		if (! var.m_SpecularFilename.empty())
+			variation.specular = var.m_SpecularFilename;	
+
 		if (! var.m_ModelFilename.empty())
 			variation.model = var.m_ModelFilename;
 
Index: source/graphics/TerrainProperties.h
===================================================================
--- source/graphics/TerrainProperties.h	(revision 11919)
+++ source/graphics/TerrainProperties.h	(working copy)
@@ -66,6 +66,10 @@
 	GroupVector m_Groups;
 
 	void LoadXml(XMBElement node, CXeromyces *pFile, const VfsPath& pathname);
+	
+	VfsPath m_NormalMap;
+	
+	VfsPath m_SpecularMap;
 
 public:
 	CTerrainProperties(CTerrainPropertiesPtr parent);
@@ -111,6 +115,26 @@
 	{
 		return m_Groups;
 	}
+	
+	const VfsPath &GetNormalMap() const
+	{
+		return m_NormalMap;
+	}
+	
+	const VfsPath &GetSpecularMap() const
+	{
+		return m_SpecularMap;
+	}
+	
+	bool HasNormalMap()
+	{
+		return !GetNormalMap().empty();
+	}
+	
+	bool HasSpecularMap()
+	{
+		return !GetSpecularMap().empty();
+	}
 };
 
 #endif
Index: source/graphics/ObjectEntry.cpp
===================================================================
--- source/graphics/ObjectEntry.cpp	(revision 11919)
+++ source/graphics/ObjectEntry.cpp	(working copy)
@@ -61,6 +61,8 @@
 	// Copy the chosen data onto this model:
 
 	m_TextureName = variation.texture;
+	m_NormalName = variation.normal;
+	m_SpecularName = variation.specular;
 	m_ModelName = variation.model;
 
 	if (! variation.color.empty())
@@ -128,6 +130,18 @@
 	texture->Prefetch(); // if we've loaded this model we're probably going to render it soon, so prefetch its texture
 	model->GetMaterial().SetDiffuseTexture(texture);
 
+	CTextureProperties normalProps(m_NormalName);
+	normalProps.SetWrap(GL_CLAMP_TO_EDGE);
+	CTexturePtr normal = g_Renderer.GetTextureManager().CreateTexture(normalProps);
+	//normal->Prefetch(); // if we've loaded this model we're probably going to render it soon, so prefetch its texture
+	model->GetMaterial().SetNormalTexture(normal);
+
+	CTextureProperties specularProps(m_SpecularName);
+	specularProps.SetWrap(GL_CLAMP_TO_EDGE);
+	CTexturePtr specular = g_Renderer.GetTextureManager().CreateTexture(specularProps);
+	//specular->Prefetch(); // if we've loaded this model we're probably going to render it soon, so prefetch its texture
+	model->GetMaterial().SetSpecularTexture(specular);
+
 	// calculate initial object space bounds, based on vertex positions
 	model->CalcStaticObjectBounds();
 
Index: source/renderer/PostprocManager.h
===================================================================
--- source/renderer/PostprocManager.h	(revision 0)
+++ source/renderer/PostprocManager.h	(revision 0)
@@ -0,0 +1,44 @@
+#ifndef INCLUDED_POSTPROCMANAGER
+#define INCLUDED_POSTPROCMANAGER
+
+class PostprocManager
+{
+public:
+	
+	GLuint fbo;
+	GLuint colourTex;
+	GLuint depthTex;
+	GLuint lumaTex;
+	
+	std::vector<std::vector<CShaderTechniquePtr> > renderStages;
+	
+	int width, height;
+	
+	bool isInitialised;
+	
+	PostprocManager();
+	~PostprocManager();
+	
+	void Initialize();
+	
+	void RecreateBuffers();
+	
+	void LoadEffect(const char* name, int stage);
+	
+	void ApplyEffect(CShaderTechniquePtr &shaderTech1);
+	void ApplyPostproc(int stage);
+	
+	void CaptureRenderOutput();
+	
+	void PostprocStage1();
+	void PostprocStage2();
+	
+	void ReleaseRenderOutput();
+	
+	
+	
+
+};
+
+
+#endif //INCLUDED_POSTPROCMANAGER
Index: source/renderer/PatchRData.h
===================================================================
--- source/renderer/PatchRData.h	(revision 11919)
+++ source/renderer/PatchRData.h	(working copy)
@@ -72,8 +72,9 @@
 		CVector3D m_Position;
 		// diffuse color from sunlight
 		SColor4ub m_DiffuseColor;
+		CVector3D m_Normal;
 	};
-	cassert(sizeof(SBaseVertex) == 16);
+	cassert(sizeof(SBaseVertex) == 28);
 
 	struct SSideVertex {
 		// vertex position
@@ -91,9 +92,10 @@
 		// vertex uvs for alpha texture
 		float m_AlphaUVs[2];
 		// add some padding since VBOs prefer power-of-two sizes
-		u32 m_Padding[2];
+		//u32 m_Padding[2];
+		CVector3D m_Normal;
 	};
-	cassert(sizeof(SBlendVertex) == 32);
+	cassert(sizeof(SBlendVertex) == 36);
 
 	// Mixed Fancy/Simple water vertex description data structure
 	struct SWaterVertex {
Index: source/renderer/RenderModifiers.h
===================================================================
--- source/renderer/RenderModifiers.h	(revision 11919)
+++ source/renderer/RenderModifiers.h	(working copy)
@@ -73,6 +73,10 @@
 	 */
 	virtual void PrepareTexture(const CShaderProgramPtr& shader, CTexture& texture) = 0;
 
+	virtual void PrepareNormal(const CShaderProgramPtr& shader, CTexture& normal) = 0;
+
+	virtual void PrepareSpecular(const CShaderProgramPtr& shader, CTexture& specular) = 0;
+
 	/**
 	 * PrepareModel: Called before rendering the given model.
 	 *
@@ -134,6 +138,8 @@
 	// Implementation
 	void BeginPass(const CShaderProgramPtr& shader);
 	void PrepareTexture(const CShaderProgramPtr& shader, CTexture& texture);
+	void PrepareNormal(const CShaderProgramPtr& shader, CTexture& normal);
+	void PrepareSpecular(const CShaderProgramPtr& shader, CTexture& specular);
 	void PrepareModel(const CShaderProgramPtr& shader, CModel* model);
 
 private:
@@ -141,6 +147,8 @@
 	CShaderProgram::Binding m_BindingShadingColor;
 	CShaderProgram::Binding m_BindingPlayerColor;
 	CShaderProgram::Binding m_BindingBaseTex;
+	CShaderProgram::Binding m_BindingNormTex;
+	CShaderProgram::Binding m_BindingSpecTex;
 };
 
 #endif // INCLUDED_RENDERMODIFIERS
Index: source/renderer/ModelRenderer.cpp
===================================================================
--- source/renderer/ModelRenderer.cpp	(revision 11919)
+++ source/renderer/ModelRenderer.cpp	(working copy)
@@ -35,6 +35,7 @@
 #include "renderer/ModelVertexRenderer.h"
 #include "renderer/Renderer.h"
 #include "renderer/RenderModifiers.h"
+#include "renderer/MikktspaceWrap.h"
 
 #include <boost/weak_ptr.hpp>
 
@@ -57,6 +58,13 @@
 #endif
 }
 
+void ModelRenderer::GenTangents(const CModelDefPtr& mdef, std::vector<float>& newVertices)
+{
+	MikkTSpace ms(mdef, newVertices);
+
+	ms.generate();
+}
+
 // Helper function to copy object-space position and normal vectors into arrays.
 void ModelRenderer::CopyPositionAndNormals(
 		const CModelDefPtr& mdef,
@@ -557,6 +565,8 @@
 				m->vertexRenderer->BeginPass(streamflags);
 
 				CTexture* currentTex = NULL;
+				CTexture* currentNorm = NULL;
+				CTexture* currentSpec = NULL;
 				CModelDef* currentModeldef = NULL;
 				CShaderUniforms currentStaticUniforms;
 				// (Texture needs to be rebound after binding a new shader, so we
@@ -581,6 +591,20 @@
 							modifier->PrepareTexture(shader, *currentTex);
 						}
 
+						CTexture* newNorm = model->GetMaterial().GetNormalTexture().get();
+						if (newNorm != currentNorm)
+						{
+							currentNorm = newNorm;
+							modifier->PrepareNormal(shader, *currentNorm);
+						}	
+
+						CTexture* newSpec = model->GetMaterial().GetSpecularTexture().get();
+						if (newSpec != currentSpec)
+						{
+							currentSpec = newSpec;
+							modifier->PrepareSpecular(shader, *currentSpec);
+						}	
+
 						// Bind modeldef when it changes
 						CModelDef* newModeldef = model->GetModelDef().get();
 						if (newModeldef != currentModeldef)
Index: source/renderer/MikktspaceWrap.h
===================================================================
--- source/renderer/MikktspaceWrap.h	(revision 0)
+++ source/renderer/MikktspaceWrap.h	(revision 0)
@@ -0,0 +1,80 @@
+#ifndef INCLUDED_MIKKWRAP
+#define INCLUDED_MIKKWRAP
+
+
+#include <graphics/mikktspace.h>
+
+class MikkTSpace
+{
+
+public:
+	
+	MikkTSpace(const CModelDefPtr& m, std::vector<float>& v);
+
+	void generate();
+	
+private:
+	
+	SMikkTSpaceInterface interface;
+	SMikkTSpaceContext context;
+
+	const CModelDefPtr& model;
+
+	std::vector<float>& newVertices;
+	
+
+	// Returns the number of faces (triangles/quads) on the mesh to be processed.
+	static int getNumFaces(const SMikkTSpaceContext *pContext);
+
+
+	// Returns the number of vertices on face number iFace
+	// iFace is a number in the range {0, 1, ..., getNumFaces()-1}
+	static int getNumVerticesOfFace(const SMikkTSpaceContext *pContext, const int iFace);
+
+
+	// returns the position/normal/texcoord of the referenced face of vertex number iVert.
+	// iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads.
+	static void getPosition(const SMikkTSpaceContext *pContext, 
+			float fvPosOut[], const int iFace, const int iVert);
+
+	static void getNormal(const SMikkTSpaceContext *pContext, 
+			float fvNormOut[], const int iFace, const int iVert);
+
+	static void getTexCoord(const SMikkTSpaceContext *pContext, 
+			float fvTexcOut[], const int iFace, const int iVert);
+
+
+	// either (or both) of the two setTSpace callbacks can be set.
+	// The call-back m_setTSpaceBasic() is sufficient for basic normal mapping.
+
+	// This function is used to return the tangent and fSign to the application.
+	// fvTangent is a unit length vector.
+	// For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level.
+	// bitangent = fSign * cross(vN, tangent);
+	// Note that the results are returned unindexed. It is possible to generate a new index list
+	// But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results.
+	// DO NOT! use an already existing index list.
+	//void setTSpaceBasic(const MikkTSpace *parent, const SMikkTSpaceContext *pContext, 
+	//		const float fvTangent[], const float fSign, const int iFace, const int iVert);
+
+
+	// This function is used to return tangent space results to the application.
+	// fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their
+	// true magnitudes which can be used for relief mapping effects.
+	// fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent.
+	// However, both are perpendicular to the vertex normal.
+	// For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level.
+	// fSign = bIsOrientationPreserving ? 1.0f : (-1.0f);
+	// bitangent = fSign * cross(vN, tangent);
+	// Note that the results are returned unindexed. It is possible to generate a new index list
+	// But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results.
+	// DO NOT! use an already existing index list.
+	static void setTSpace(const SMikkTSpaceContext * pContext, const float fvTangent[], 
+			const float fvBiTangent[], const float fMagS, const float fMagT, 
+			const tbool bIsOrientationPreserving, const int iFace, const int iVert);
+
+
+};
+
+
+#endif // INCLUDED_MIKKWRAP
\ No newline at end of file
Index: source/renderer/PostprocManager.cpp
===================================================================
--- source/renderer/PostprocManager.cpp	(revision 0)
+++ source/renderer/PostprocManager.cpp	(revision 0)
@@ -0,0 +1,242 @@
+#include "precompiled.h"
+
+#include "lib/ogl.h"
+#include "maths/MathUtil.h"
+
+#include "gui/GUIutil.h"
+#include "lib/bits.h"
+
+#include "graphics/ShaderManager.h"
+#include "renderer/Renderer.h"
+
+#include "renderer/PostprocManager.h"
+
+
+PostprocManager::PostprocManager()
+{
+	isInitialised = false;
+	fbo = 0;
+	colourTex = 0;
+	depthTex = 0;
+	lumaTex = 0;	
+}
+
+
+PostprocManager::~PostprocManager()
+{
+	if (isInitialised)
+	{
+		if (fbo) pglDeleteFramebuffersEXT(1, &fbo);
+		if (colourTex) glDeleteTextures(1, &colourTex);
+		if (depthTex) glDeleteTextures(1, &depthTex);
+		if (lumaTex) glDeleteTextures(1, &lumaTex);
+	}
+}
+
+void PostprocManager::Initialize()
+{
+	RecreateBuffers();
+	isInitialised = true;
+	
+	renderStages = std::vector<std::vector<CShaderTechniquePtr> >(5);
+	
+	LoadEffect("ssao", 0);
+	
+	LoadEffect("hdr",   1);
+	LoadEffect("bloom", 1);
+}
+
+
+void PostprocManager::RecreateBuffers()
+{
+	if (isInitialised)
+	{
+		if (fbo) pglDeleteFramebuffersEXT(1, &fbo);
+		if (colourTex) glDeleteTextures(1, &colourTex);
+		if (depthTex) glDeleteTextures(1, &depthTex);
+		if (lumaTex) glDeleteTextures(1, &lumaTex);
+	}
+		
+	
+	/*width = height = (int)round_up_to_pow2((unsigned)std::max(g_Renderer.GetWidth(), g_Renderer.GetHeight()));
+	width = std::min(width, (int)ogl_max_tex_size);
+	height = std::min(height, (int)ogl_max_tex_size);*/
+	
+	width = g_Renderer.GetWidth();
+	height = g_Renderer.GetHeight();
+
+	
+	glGenTextures(1, (GLuint*)&colourTex);
+	glBindTexture(GL_TEXTURE_2D, colourTex);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA,
+			GL_HALF_FLOAT, 0);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+
+
+	glGenTextures(1, (GLuint*)&depthTex);
+	glBindTexture(GL_TEXTURE_2D, depthTex);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, width, height,
+		      0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE,NULL);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE,
+			GL_NONE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+	
+
+	glGenTextures(1, (GLuint*)&lumaTex);
+	glBindTexture(GL_TEXTURE_2D, lumaTex);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE16, width, height,
+		  0, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+	
+	glBindTexture(GL_TEXTURE_2D, 0);
+
+	/**pglGenFramebuffersEXT(1, &fbo);
+	pglBindFramebufferEXT(GL_FRAMEBUFFER, fbo);
+	pglFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colourTex, 0);
+	pglFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
+	//glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT, GL_TEXTURE_2D, colourTex, 0);
+
+	pglBindFramebufferEXT(GL_FRAMEBUFFER, 0);*/
+}
+
+
+void PostprocManager::CaptureRenderOutput()
+{
+	glBindTexture(GL_TEXTURE_2D, colourTex);	
+	glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 0, 0, width, height, 0);
+	glBindTexture(GL_TEXTURE_2D, depthTex);
+	glCopyTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT, 0, 0, width, height, 0);
+	glBindTexture(GL_TEXTURE_2D, lumaTex);
+	glCopyTexImage2D(GL_TEXTURE_2D,0,GL_LUMINANCE16, 0, 0, width, height, 0);
+}
+
+
+void PostprocManager::ReleaseRenderOutput()
+{
+	//pglBindFramebufferEXT(GL_FRAMEBUFFER, 0);
+}
+
+void PostprocManager::LoadEffect(const char* name, int stage)
+{
+	CShaderTechniquePtr shaderTech1 = g_Renderer.GetShaderManager().LoadEffect(name);
+	
+	renderStages[stage].push_back(shaderTech1);
+	
+}
+
+void PostprocManager::ApplyEffect(CShaderTechniquePtr &shaderTech1)
+{
+	CaptureRenderOutput();
+	
+	glDisable(GL_DEPTH_TEST);
+	glDepthMask(GL_FALSE);
+
+	shaderTech1->BeginPass();
+	CShaderProgramPtr shader = shaderTech1->GetShader();
+	
+	shader->Bind();	
+	
+	shader->BindTexture("bgl_RenderedTexture", colourTex);
+	
+	shader->BindTexture("bgl_DepthTexture", depthTex);
+	
+	shader->BindTexture("bgl_LuminanceTexture", lumaTex);
+	
+	shader->Uniform("bgl_RenderedTextureWidth", width);
+	shader->Uniform("bgl_RenderedTextureHeight", height);
+	
+	glBegin(GL_QUADS);
+	    glColor4f(1.f, 1.f, 1.f, 1.f);
+	    glTexCoord2f(1.0, 1.0);	glVertex2f(1,1);
+	    glTexCoord2f(0.0, 1.0);	glVertex2f(-1,1);	    
+	    glTexCoord2f(0.0, 0.0);	glVertex2f(-1,-1);
+	    glTexCoord2f(1.0, 0.0);	glVertex2f(1,-1);
+	glEnd();
+	
+	shader->Unbind();
+	
+	shaderTech1->EndPass();	
+		
+	glDepthMask(GL_TRUE);
+	glEnable(GL_DEPTH_TEST);
+}
+
+
+
+void PostprocManager::ApplyPostproc(int stage)
+{
+	if (!isInitialised) 
+		return;
+	
+	std::vector<CShaderTechniquePtr> &effects = renderStages[stage];
+	
+	//std::cout << "EFFECT!!! " << effects.size() << std::endl;
+	
+	for (size_t i = 0; i < effects.size(); i++)
+	{		
+		ApplyEffect(effects[i]);
+	}
+}
+
+/*void PostprocManager::PostprocStage2()
+{
+	CShaderTechniquePtr shaderTech2 = g_Renderer.GetShaderManager().LoadEffect("hdr");
+		
+	shaderTech2->BeginPass();
+	CShaderProgramPtr shader2 = shaderTech2->GetShader();
+	
+	shader2->Bind();
+	
+	shader2->BindTexture("bgl_RenderedTexture", texname[0]);
+	shader2->BindTexture("bgl_DepthTexture", texname[1]);
+	shader2->BindTexture("bgl_LuminanceTexture", texname[2]);
+	
+	glBegin(GL_QUADS);
+	    glColor4f(1.f, 1.f, 1.f, 1.f);
+	    glTexCoord2f(1.0, 1.0);	glVertex2f(1,1);
+	    glTexCoord2f(0.0, 1.0);	glVertex2f(-1,1);
+	    //glTexCoord2f(1.0, 1.0);	glVertex2f(1,0);
+	    //glTexCoord2f(0.0, 1.0);	glVertex2f(-1,0);
+	    glTexCoord2f(0.0, 0.0);	glVertex2f(-1,-1);
+	    glTexCoord2f(1.0, 0.0);	glVertex2f(1,-1);
+	glEnd();
+	
+	shaderTech2->EndPass();
+	shader2->Unbind();
+	
+	
+		
+	
+	CShaderTechniquePtr shaderTech3 = g_Renderer.GetShaderManager().LoadEffect("bloom");
+		
+	shaderTech3->BeginPass();
+	CShaderProgramPtr shader3 = shaderTech3->GetShader();
+	
+	shader3->Bind();
+	
+	shader3->BindTexture("bgl_RenderedTexture", texname[0]);
+	shader3->BindTexture("bgl_DepthTexture", texname[1]);
+	shader3->BindTexture("bgl_LuminanceTexture", texname[2]);
+	
+	glBegin(GL_QUADS);
+	    glColor4f(1.f, 1.f, 1.f, 1.f);
+	    glTexCoord2f(1.0, 1.0);	glVertex2f(1,1);
+	    glTexCoord2f(0.0, 1.0);	glVertex2f(-1,1);
+	    //glTexCoord2f(1.0, 1.0);	glVertex2f(1,0);
+	    //glTexCoord2f(0.0, 1.0);	glVertex2f(-1,0);
+	    glTexCoord2f(0.0, 0.0);	glVertex2f(-1,-1);
+	    glTexCoord2f(1.0, 0.0);	glVertex2f(1,-1);
+	glEnd();
+	
+	shaderTech3->EndPass();
+	shader3->Unbind();
+}*/
\ No newline at end of file
Index: source/renderer/InstancingModelRenderer.cpp
===================================================================
--- source/renderer/InstancingModelRenderer.cpp	(revision 11919)
+++ source/renderer/InstancingModelRenderer.cpp	(working copy)
@@ -31,6 +31,7 @@
 #include "graphics/LightEnv.h"
 #include "graphics/Model.h"
 #include "graphics/ModelDef.h"
+#include "graphics/weldmesh.h"
 
 #include "renderer/InstancingModelRenderer.h"
 #include "renderer/Renderer.h"
@@ -49,6 +50,7 @@
 	/// Position, normals and UV are all static
 	VertexArray::Attribute m_Position;
 	VertexArray::Attribute m_Normal;
+	VertexArray::Attribute m_Tangent;
 	VertexArray::Attribute m_UV;
 	VertexArray::Attribute m_BlendJoints; // valid iff gpuSkinning == true
 	VertexArray::Attribute m_BlendWeights; // valid iff gpuSkinning == true
@@ -76,6 +78,10 @@
 	m_UV.type = GL_FLOAT;
 	m_UV.elems = 2;
 	m_Array.AddAttribute(&m_UV);
+	
+	m_Tangent.type = GL_FLOAT;
+	m_Tangent.elems = 4;
+	m_Array.AddAttribute(&m_Tangent);
 
 	if (gpuSkinning)
 	{
@@ -87,16 +93,51 @@
 		m_BlendWeights.elems = 4;
 		m_Array.AddAttribute(&m_BlendWeights);
 	}
+	
 
-	m_Array.SetNumVertices(numVertices);
+	std::vector<float> newVertices;
+	
+	SModelVertex* vertices = mdef->GetVertices();
+	
+	ModelRenderer::GenTangents(mdef, newVertices);
+	
+	int numVertexAttrs = 3 + 3 + 2 + 4;
+	
+	int vert = newVertices.size() / numVertexAttrs;
+	
+	std::vector<int> remapTable(vert);
+	std::vector<float> vertexDataIn(vert * numVertexAttrs); 
+	std::vector<float> vertexDataOut(vert * numVertexAttrs);
+		
+	vertexDataIn = newVertices;
+
+	int numVertices2 = WeldMesh(&remapTable[0], &vertexDataOut[0],
+			  &vertexDataIn[0], vert, numVertexAttrs);
+
+	m_Array.SetNumVertices(numVertices2);
 	m_Array.Layout();
 
 	VertexArrayIterator<CVector3D> Position = m_Position.GetIterator<CVector3D>();
 	VertexArrayIterator<CVector3D> Normal = m_Normal.GetIterator<CVector3D>();
 	VertexArrayIterator<float[2]> UVit = m_UV.GetIterator<float[2]>();
+	VertexArrayIterator<CVector4D> Tangent = m_Tangent.GetIterator<CVector4D>();
 
-	ModelRenderer::CopyPositionAndNormals(mdef, Position, Normal);
-	ModelRenderer::BuildUV(mdef, UVit);
+	for (int i = 0; i < numVertices2; i++)
+	{	
+		int p = remapTable[i];
+		
+		int q = numVertexAttrs * i;
+		
+		Position[i] = CVector3D(vertexDataOut[q + 0], vertexDataOut[q + 1], vertexDataOut[q + 2]);
+		
+		Normal[i] = CVector3D(vertexDataOut[q + 3], vertexDataOut[q + 4], vertexDataOut[q + 5]);
+		
+		UVit[i][0] = vertexDataOut[q + 6];
+		UVit[i][1] = vertexDataOut[q + 7];
+		
+		Tangent[i] = CVector4D(vertexDataOut[q + 8], vertexDataOut[q + 9], vertexDataOut[q + 10], 
+				       vertexDataOut[q + 11]);
+	}
 
 	if (gpuSkinning)
 	{
@@ -118,7 +159,20 @@
 
 	m_IndexArray.SetNumVertices(mdef->GetNumFaces()*3);
 	m_IndexArray.Layout();
-	ModelRenderer::BuildIndices(mdef, m_IndexArray.GetIterator());
+	
+	VertexArrayIterator<u16> Indices = m_IndexArray.GetIterator();
+	
+	size_t idxidx = 0;
+	SModelFace* faces = mdef->GetFaces();	
+
+	for (size_t j = 0; j < mdef->GetNumFaces(); ++j) {	
+		Indices[idxidx++]=remapTable[j * 3 + 0];
+		Indices[idxidx++]=remapTable[j * 3 + 1];
+		Indices[idxidx++]=remapTable[j * 3 + 2];
+	}
+	
+	
+	
 	m_IndexArray.Upload();
 	m_IndexArray.FreeBackingStore();
 }
@@ -211,6 +265,8 @@
 	if (streamflags & STREAM_UV0)
 		shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, stride, base + m->imodeldef->m_UV.offset);
 
+	shader->VertexAttribPointer("a_tangent", 4, GL_FLOAT, GL_TRUE, stride, base + m->imodeldef->m_Tangent.offset);
+
 	// GPU skinning requires extra attributes to compute positions/normals
 	if (m->gpuSkinning)
 	{
Index: source/renderer/Renderer.cpp
===================================================================
--- source/renderer/Renderer.cpp	(revision 11919)
+++ source/renderer/Renderer.cpp	(working copy)
@@ -66,6 +66,7 @@
 #include "renderer/TerrainRenderer.h"
 #include "renderer/VertexBufferManager.h"
 #include "renderer/WaterManager.h"
+#include "renderer/PostprocManager.h"
 
 
 ///////////////////////////////////////////////////////////////////////////////////
@@ -271,6 +272,8 @@
 
 	/// Shadow map
 	ShadowMap shadow;
+	
+	PostprocManager postprocManager;
 
 	/// Various model renderers
 	struct Models
@@ -631,6 +634,8 @@
 	// Let component renderers perform one-time initialization after graphics capabilities and
 	// the shader path have been determined.
 	m->overlayRenderer.Initialize();
+	
+	m->postprocManager.Initialize();
 
 	return true;
 }
@@ -639,10 +644,12 @@
 void CRenderer::Resize(int width,int height)
 {
 	// need to recreate the shadow map object to resize the shadow texture
-	m->shadow.RecreateTexture();
+	m->shadow.RecreateTexture();	
 
 	m_Width = width;
 	m_Height = height;
+	
+	m->postprocManager.RecreateBuffers();
 }
 
 //////////////////////////////////////////////////////////////////////////////////////////
@@ -705,7 +712,7 @@
 // SetRenderPath: Select the preferred render path.
 // This may only be called before Open(), because the layout of vertex arrays and other
 // data may depend on the chosen render path.
-void CRenderer::SetRenderPath(RenderPath rp)
+void CRenderer::SetRenderPath(RenderPath rp) 
 {
 	if (!m->IsOpen)
 	{
@@ -1416,6 +1423,11 @@
 		}
 	}
 
+	RenderModels(context);
+	ogl_WarnIfError();	
+
+	m->postprocManager.ApplyPostproc(0);
+
 	// render submitted patches and models
 	RenderPatches(context);
 	ogl_WarnIfError();
@@ -1428,9 +1440,6 @@
 	m->overlayRenderer.RenderOverlaysBeforeWater();
 	ogl_WarnIfError();
 
-	RenderModels(context);
-	ogl_WarnIfError();
-
 	// render water
 	if (m_WaterManager->m_RenderWater && g_Game && waterScissor.GetVolume() > 0)
 	{
@@ -1452,6 +1461,8 @@
 		ogl_WarnIfError();
 	}
 
+	m->postprocManager.ApplyPostproc(1);
+
 	// render debug-related terrain overlays
 	ITerrainOverlay::RenderOverlaysAfterWater();
 	ogl_WarnIfError();
Index: source/renderer/PatchRData.cpp
===================================================================
--- source/renderer/PatchRData.cpp	(revision 11919)
+++ source/renderer/PatchRData.cpp	(working copy)
@@ -367,6 +367,7 @@
 
 	terrain->CalcPosition(gx, gz, dst.m_Position);
 	terrain->CalcNormal(gx, gz, normal);
+	dst.m_Normal = normal;
 	dst.m_DiffuseColor = cpuLighting ? lightEnv.EvaluateTerrainDiffuseScaled(normal) : lightEnv.EvaluateTerrainDiffuseFactor(normal);
 	dst.m_AlphaUVs[0] = vtx[0].m_AlphaUVs[0];
 	dst.m_AlphaUVs[1] = vtx[0].m_AlphaUVs[1];
@@ -374,6 +375,7 @@
 
 	terrain->CalcPosition(gx + 1, gz, dst.m_Position);
 	terrain->CalcNormal(gx + 1, gz, normal);
+	dst.m_Normal = normal;
 	dst.m_DiffuseColor = cpuLighting ? lightEnv.EvaluateTerrainDiffuseScaled(normal) : lightEnv.EvaluateTerrainDiffuseFactor(normal);
 	dst.m_AlphaUVs[0] = vtx[1].m_AlphaUVs[0];
 	dst.m_AlphaUVs[1] = vtx[1].m_AlphaUVs[1];
@@ -381,6 +383,7 @@
 
 	terrain->CalcPosition(gx + 1, gz + 1, dst.m_Position);
 	terrain->CalcNormal(gx + 1, gz + 1, normal);
+	dst.m_Normal = normal;
 	dst.m_DiffuseColor = cpuLighting ? lightEnv.EvaluateTerrainDiffuseScaled(normal) : lightEnv.EvaluateTerrainDiffuseFactor(normal);
 	dst.m_AlphaUVs[0] = vtx[2].m_AlphaUVs[0];
 	dst.m_AlphaUVs[1] = vtx[2].m_AlphaUVs[1];
@@ -388,6 +391,7 @@
 
 	terrain->CalcPosition(gx, gz + 1, dst.m_Position);
 	terrain->CalcNormal(gx, gz + 1, normal);
+	dst.m_Normal = normal;
 	dst.m_DiffuseColor = cpuLighting ? lightEnv.EvaluateTerrainDiffuseScaled(normal) : lightEnv.EvaluateTerrainDiffuseFactor(normal);
 	dst.m_AlphaUVs[0] = vtx[3].m_AlphaUVs[0];
 	dst.m_AlphaUVs[1] = vtx[3].m_AlphaUVs[1];
@@ -546,6 +550,8 @@
 			// for all vertices, it need not be stored in the vertex structure)
 			CVector3D normal;
 			terrain->CalcNormal(ix,iz,normal);
+			
+			vertices[v].m_Normal = normal;
 
 			vertices[v].m_DiffuseColor = cpuLighting ? lightEnv.EvaluateTerrainDiffuseScaled(normal) : lightEnv.EvaluateTerrainDiffuseFactor(normal);
 		}
@@ -747,6 +753,15 @@
 		if (itt->first)
 		{
 			shader->BindTexture("baseTex", itt->first->GetTexture());
+			
+			shader->Uniform("hasNormalMap", itt->first->GetNormal() ? CVector3D(1,0,0) : CVector3D(0,0,0));
+			shader->Uniform("hasSpecularMap", itt->first->GetSpecular() ? CVector3D(1,0,0) : CVector3D(0,0,0));
+			
+			if (itt->first->GetNormal())	
+				shader->BindTexture("normTex", itt->first->GetNormal());
+			
+			if (itt->first->GetSpecular())	
+				shader->BindTexture("specTex", itt->first->GetSpecular());
 
 #if !CONFIG2_GLES
 			if (isDummyShader)
@@ -774,6 +789,7 @@
 			SBaseVertex *base = (SBaseVertex *)itv->first->Bind();
 			shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position[0]);
 			shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, &base->m_DiffuseColor);
+			shader->NormalPointer(GL_FLOAT, stride, &base->m_Normal[0]);				
 			shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position[0]);
 
 			shader->AssertPointersBound();
@@ -932,6 +948,16 @@
 		if (itt->m_Texture)
 		{
 			shader->BindTexture("baseTex", itt->m_Texture->GetTexture());
+			
+			shader->Uniform("hasNormalMap", itt->m_Texture->GetNormal() ? CVector3D(1,0,0) : CVector3D(0,0,0));
+			shader->Uniform("hasSpecularMap", itt->m_Texture->GetSpecular() ? CVector3D(1,0,0) : CVector3D(0,0,0));
+			
+			if (itt->m_Texture->GetNormal())
+				shader->BindTexture("normTex", itt->m_Texture->GetNormal());
+			
+			if (itt->m_Texture->GetSpecular())
+				shader->BindTexture("specTex", itt->m_Texture->GetSpecular());
+			
 
 #if !CONFIG2_GLES
 			if (isDummyShader)
@@ -965,6 +991,7 @@
 
 				shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position[0]);
 				shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, &base->m_DiffuseColor);
+				shader->NormalPointer(GL_FLOAT, stride, &base->m_Normal[0]);
 				shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position[0]);
 				shader->TexCoordPointer(GL_TEXTURE1, 2, GL_FLOAT, stride, &base->m_AlphaUVs[0]);
 			}
Index: source/renderer/RenderModifiers.cpp
===================================================================
--- source/renderer/RenderModifiers.cpp	(revision 11919)
+++ source/renderer/RenderModifiers.cpp	(working copy)
@@ -104,6 +104,8 @@
 	m_BindingShadingColor = shader->GetUniformBinding("shadingColor");
 	m_BindingPlayerColor = shader->GetUniformBinding("playerColor");
 	m_BindingBaseTex = shader->GetTextureBinding("baseTex");
+	m_BindingNormTex = shader->GetTextureBinding("normTex");
+	m_BindingSpecTex = shader->GetTextureBinding("specTex");
 }
 
 void ShaderRenderModifier::PrepareTexture(const CShaderProgramPtr& shader, CTexture& texture)
@@ -112,6 +114,18 @@
 		shader->BindTexture(m_BindingBaseTex, texture.GetHandle());
 }
 
+void ShaderRenderModifier::PrepareNormal(const CShaderProgramPtr& shader, CTexture& normal)
+{
+	if (m_BindingNormTex.Active())
+		shader->BindTexture(m_BindingNormTex, normal.GetHandle());
+}
+
+void ShaderRenderModifier::PrepareSpecular(const CShaderProgramPtr& shader, CTexture& specular)
+{
+	if (m_BindingSpecTex.Active())
+		shader->BindTexture(m_BindingSpecTex, specular.GetHandle());
+}
+
 void ShaderRenderModifier::PrepareModel(const CShaderProgramPtr& shader, CModel* model)
 {
 	if (m_BindingInstancingTransform.Active())
Index: source/renderer/MikktspaceWrap.cpp
===================================================================
--- source/renderer/MikktspaceWrap.cpp	(revision 0)
+++ source/renderer/MikktspaceWrap.cpp	(revision 0)
@@ -0,0 +1,118 @@
+
+#include "precompiled.h"
+
+#include <boost/bind.hpp>
+
+#include "graphics/Color.h"
+#include "graphics/LightEnv.h"
+#include "graphics/Model.h"
+#include "graphics/ModelDef.h"
+#include "graphics/ShaderManager.h"
+#include "graphics/TextureManager.h"
+#include <graphics/mikktspace.h>
+
+#include <renderer/MikktspaceWrap.h>
+
+
+
+MikkTSpace::MikkTSpace(const CModelDefPtr& m, std::vector<float>& v) : model(m), newVertices(v)
+{	
+	interface.m_getNumFaces = getNumFaces;
+	interface.m_getNumVerticesOfFace = getNumVerticesOfFace;
+	interface.m_getPosition = getPosition;
+	interface.m_getNormal = getNormal;
+	interface.m_getTexCoord = getTexCoord;
+	interface.m_setTSpaceBasic = NULL;
+	interface.m_setTSpace = setTSpace;
+
+	context.m_pInterface = &interface;
+	context.m_pUserData = (void*)this;
+}
+
+void MikkTSpace::generate()
+{
+	genTangSpaceDefault(&context);
+}
+
+
+int MikkTSpace::getNumFaces(const SMikkTSpaceContext *pContext)
+{
+	return ((MikkTSpace*)pContext->m_pUserData)->model->GetNumFaces();
+}
+
+
+int MikkTSpace::getNumVerticesOfFace(const SMikkTSpaceContext *pContext, const int iFace)
+{
+	return 3;
+}
+
+
+void MikkTSpace::getPosition(const SMikkTSpaceContext *pContext, 
+		float fvPosOut[], const int iFace, const int iVert)
+{
+	SModelFace &face = ((MikkTSpace*)pContext->m_pUserData)->model->GetFaces()[iFace];
+	long i = face.m_Verts[iVert];
+	const CVector3D &p = ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i].m_Coords;
+
+	fvPosOut[0] = p.X;
+	fvPosOut[1] = p.Y;
+	fvPosOut[2] = p.Z;
+}
+
+
+void MikkTSpace::getNormal(const SMikkTSpaceContext *pContext, 
+		float fvNormOut[], const int iFace, const int iVert)
+{
+	SModelFace &face = ((MikkTSpace*)pContext->m_pUserData)->model->GetFaces()[iFace];
+	long i = face.m_Verts[iVert];
+	const CVector3D &n = ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i].m_Norm;
+
+	fvNormOut[0] = n.X;
+	fvNormOut[1] = n.Y;
+	fvNormOut[2] = n.Z;
+}
+
+
+void MikkTSpace::getTexCoord(const SMikkTSpaceContext *pContext, 
+		float fvTexcOut[], const int iFace, const int iVert)
+{
+	SModelFace &face = ((MikkTSpace*)pContext->m_pUserData)->model->GetFaces()[iFace];
+	long i = face.m_Verts[iVert];
+	SModelVertex &v = ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i];
+
+	fvTexcOut[0] = v.m_U;
+	fvTexcOut[1] = 1.0-v.m_V;		
+}
+
+
+void MikkTSpace::setTSpace(const SMikkTSpaceContext * pContext, const float fvTangent[], 
+		const float fvBiTangent[], const float fMagS, const float fMagT, 
+		const tbool bIsOrientationPreserving, const int iFace, const int iVert)
+{
+	SModelFace &face = ((MikkTSpace*)pContext->m_pUserData)->model->GetFaces()[iFace];
+	long i = face.m_Verts[iVert];
+	
+	const CVector3D &p = ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i].m_Coords;
+	const CVector3D &n = ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i].m_Norm;
+	const float u = ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i].m_U;
+	const float v = 1.0 - ((MikkTSpace*)pContext->m_pUserData)->model->GetVertices()[i].m_V;
+	
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(p.X);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(p.Y);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(p.Z);
+	
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(n.X);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(n.Y);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(n.Z);
+	
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(u);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(v);
+	
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(fvTangent[0]);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(fvTangent[1]);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(fvTangent[2]);
+	((MikkTSpace*)pContext->m_pUserData)->newVertices.push_back(bIsOrientationPreserving > 0.5 ? 1.0f : (-1.0f));
+}
+
+
+
Index: source/renderer/ModelRenderer.h
===================================================================
--- source/renderer/ModelRenderer.h	(revision 11919)
+++ source/renderer/ModelRenderer.h	(working copy)
@@ -253,6 +253,9 @@
 	static void BuildIndices(
 			const CModelDefPtr& mdef,
 			const VertexArrayIterator<u16>& Indices);
+
+
+	static void GenTangents(const CModelDefPtr& mdef, std::vector<float>& newVertices);
 };
 
 
Index: binaries/data/mods/public/maps/scenarios/Hellenised_Egypt.xml
===================================================================
--- binaries/data/mods/public/maps/scenarios/Hellenised_Egypt.xml	(revision 11919)
+++ binaries/data/mods/public/maps/scenarios/Hellenised_Egypt.xml	(working copy)
@@ -4,11 +4,11 @@
 	<Environment>
 		<LightingModel>standard</LightingModel>
 		<SkySet>sunny 1</SkySet>
-		<SunColour r="0.776471" g="0.756863" b="0.67451"/>
+		<SunColour r="0.658824" g="0.643137" b="0.572549"/>
 		<SunElevation angle="0.877437"/>
 		<SunRotation angle="-0.824594"/>
-		<TerrainAmbientColour r="0.313726" g="0.376471" b="0.521569"/>
-		<UnitsAmbientColour r="0.501961" g="0.501961" b="0.501961"/>
+		<TerrainAmbientColour r="0.235294" g="0.282353" b="0.388235"/>
+		<UnitsAmbientColour r="0.4" g="0.4" b="0.4"/>
 		<Water>
 			<WaterBody>
 				<Type>default</Type>
@@ -24,8 +24,8 @@
 		</Water>
 	</Environment>
 	<Camera>
-		<Position x="322.034" y="134.957" z="324.278"/>
-		<Rotation angle="0"/>
+		<Position x="380.081" y="159.73" z="434.628"/>
+		<Rotation angle="-1.41128"/>
 		<Declination angle="0.610865"/>
 	</Camera>
 	<ScriptSettings><![CDATA[
Index: binaries/data/mods/public/art/materials/player_trans_norm.xml
===================================================================
--- binaries/data/mods/public/art/materials/player_trans_norm.xml	(revision 0)
+++ binaries/data/mods/public/art/materials/player_trans_norm.xml	(revision 0)
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<material>
+    <shader effect="model_norm"/>
+    <define name="USE_PLAYERCOLOR" value="1"/>
+</material>
Index: binaries/data/mods/public/art/textures/terrain/types/biome-desert/desert_city_tile_pers_dirt.xml
===================================================================
--- binaries/data/mods/public/art/textures/terrain/types/biome-desert/desert_city_tile_pers_dirt.xml	(revision 11919)
+++ binaries/data/mods/public/art/textures/terrain/types/biome-desert/desert_city_tile_pers_dirt.xml	(working copy)
@@ -3,5 +3,5 @@
 <!DOCTYPE Terrains SYSTEM "/art/textures/terrain/types/terrains.dtd">
 
 <Terrains>
-	<Terrain angle="0.0" size="64.0"/>
+	<Terrain angle="0.0" size="64.0" normalmap="art/textures/terrain/types/biome-desert/desert_city_tile_pers_dirt_norm.png"/>
 </Terrains>
Index: binaries/data/mods/public/art/textures/terrain/types/biome-desert/desert_city_tile_pers_dirt.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: binaries/data/mods/public/art/textures/terrain/types/snow/snow grass 2.xml
===================================================================
--- binaries/data/mods/public/art/textures/terrain/types/snow/snow grass 2.xml	(revision 0)
+++ binaries/data/mods/public/art/textures/terrain/types/snow/snow grass 2.xml	(revision 0)
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="iso-8859-1" standalone="no"?>
+
+<!DOCTYPE Terrains SYSTEM "/art/textures/terrain/types/terrains.dtd">
+
+<Terrains>
+	<Terrain normalmap="art/textures/terrain/types/snow/snow grass 2_norm.png" specularmap="art/textures/terrain/types/snow/snow grass 2_spec.png"/>
+</Terrains>
Index: binaries/data/mods/public/art/textures/skins/structural/rome_struct_spec.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: binaries/data/mods/public/art/textures/skins/structural/rome_struct_spec.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Index: binaries/data/mods/public/art/textures/skins/structural/rome_struct_norm.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: binaries/data/mods/public/art/textures/skins/structural/rome_struct_norm.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Index: binaries/data/mods/public/art/textures/skins/structural/rome_struct_arch_spec.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: binaries/data/mods/public/art/textures/skins/structural/rome_struct_arch_spec.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Index: binaries/data/mods/public/art/textures/skins/structural/rome_struct_arch_norm.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: binaries/data/mods/public/art/textures/skins/structural/rome_struct_arch_norm.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Index: binaries/data/mods/public/art/actors/structures/romans/civic_centre.xml
===================================================================
--- binaries/data/mods/public/art/actors/structures/romans/civic_centre.xml	(revision 11919)
+++ binaries/data/mods/public/art/actors/structures/romans/civic_centre.xml	(working copy)
@@ -15,6 +15,8 @@
         <prop actor="props/units/weapons/arrow_front.xml" attachpoint="projectile"/>
       </props>
       <texture>structural/rome_struct.png</texture>
+      <normal>structural/rome_struct_norm.png</normal>
+      <specular>structural/rome_struct_spec.png</specular>
     </variant>
   </group>
   <group>
@@ -36,5 +38,5 @@
       </props>
     </variant>
   </group>
-  <material>player_trans.xml</material>
+  <material>player_trans_norm.xml</material>
 </actor>
Index: binaries/data/mods/public/art/actors/structures/romans/triumphal_arch.xml
===================================================================
--- binaries/data/mods/public/art/actors/structures/romans/triumphal_arch.xml	(revision 11919)
+++ binaries/data/mods/public/art/actors/structures/romans/triumphal_arch.xml	(working copy)
@@ -5,6 +5,8 @@
     <variant frequency="100" name="Roman Triumphal Arch">
       <mesh>structural/rome_arch.dae</mesh>
       <texture>structural/rome_struct_arch.dds</texture>
+      <normal>structural/rome_struct_arch_norm.png</normal>
+      <specular>structural/rome_struct_arch_spec.png</specular>
     </variant>
   </group>
   <group>
@@ -17,5 +19,5 @@
       </props>
     </variant>
   </group>
-  <material>player_trans.xml</material>
+  <material>player_trans_norm.xml</material>
 </actor>
Index: binaries/data/mods/public/shaders/effects/model_norm.xml
===================================================================
--- binaries/data/mods/public/shaders/effects/model_norm.xml	(revision 0)
+++ binaries/data/mods/public/shaders/effects/model_norm.xml	(revision 0)
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<effect>
+
+    <technique>
+        <require context="MODE_SHADOWCAST || MODE_SILHOUETTEOCCLUDER"/>
+        <require shaders="arb"/>
+        <pass shader="arb/model_solid"/>
+    </technique>
+
+    <technique>
+        <require context="MODE_SHADOWCAST || MODE_SILHOUETTEOCCLUDER"/>
+        <require shaders="glsl"/>
+        <pass shader="glsl/model_solid"/>
+    </technique>
+
+    <technique>
+        <require context="MODE_SHADOWCAST || MODE_SILHOUETTEOCCLUDER"/>
+        <require shaders="fixed"/>
+        <define name="USE_PLAYERCOLOR" value="0"/>
+        <define name="USE_OBJECTCOLOR" value="0"/>
+        <pass shader="fixed:model_solid"/>
+    </technique>
+
+
+    <technique>
+        <require context="MODE_SILHOUETTEDISPLAY || MODE_WIREFRAME"/>
+        <require shaders="arb"/>
+        <pass shader="arb/model_solid_player"/>
+    </technique>
+
+    <technique>
+        <require context="MODE_SILHOUETTEDISPLAY || MODE_WIREFRAME"/>
+        <require shaders="glsl"/>
+        <pass shader="glsl/model_solid_player"/>
+    </technique>
+
+    <technique>
+        <require context="MODE_SILHOUETTEDISPLAY || MODE_WIREFRAME"/>
+        <require shaders="fixed"/>
+        <define name="USE_PLAYERCOLOR" value="1"/>
+        <define name="USE_OBJECTCOLOR" value="0"/>
+        <pass shader="fixed:model_solid"/>
+    </technique>
+
+
+    <technique>
+        <require shaders="arb"/>
+        <pass shader="arb/model_common"/>
+    </technique>
+
+    <technique>
+        <require shaders="glsl"/>
+        <pass shader="glsl/model_common_norm"/>
+    </technique>
+
+    <technique>
+        <require context="USE_PLAYERCOLOR || USE_OBJECTCOLOR"/>
+        <require shaders="fixed"/>
+        <pass shader="fixed:model_color"/>
+    </technique>
+
+    <technique>
+        <require shaders="fixed"/>
+        <pass shader="fixed:model"/>
+    </technique>
+
+</effect>
Index: binaries/data/mods/public/shaders/effects/ssao.xml
===================================================================
--- binaries/data/mods/public/shaders/effects/ssao.xml	(revision 0)
+++ binaries/data/mods/public/shaders/effects/ssao.xml	(revision 0)
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<effect>
+
+    <technique>
+        <require shaders="glsl"/>
+        <pass shader="glsl/ssao"/>
+    </technique>
+
+</effect>
Index: binaries/data/mods/public/shaders/effects/bloom.xml
===================================================================
--- binaries/data/mods/public/shaders/effects/bloom.xml	(revision 0)
+++ binaries/data/mods/public/shaders/effects/bloom.xml	(revision 0)
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<effect>
+
+    <technique>
+        <require shaders="glsl"/>
+        <pass shader="glsl/bloom"/>
+    </technique>
+
+</effect>
Index: binaries/data/mods/public/shaders/effects/hdr.xml
===================================================================
--- binaries/data/mods/public/shaders/effects/hdr.xml	(revision 0)
+++ binaries/data/mods/public/shaders/effects/hdr.xml	(revision 0)
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<effect>
+
+    <technique>
+        <require shaders="glsl"/>
+        <pass shader="glsl/hdr"/>
+    </technique>
+
+</effect>
Index: binaries/data/mods/public/shaders/glsl/terrain_common.fs
===================================================================
--- binaries/data/mods/public/shaders/glsl/terrain_common.fs	(revision 11919)
+++ binaries/data/mods/public/shaders/glsl/terrain_common.fs	(working copy)
@@ -1,6 +1,8 @@
 #version 120
 
 uniform sampler2D baseTex;
+uniform sampler2D normTex;
+uniform sampler2D specTex;
 uniform sampler2D blendTex;
 uniform sampler2D losTex;
 
@@ -17,13 +19,22 @@
 
 uniform vec3 shadingColor;
 uniform vec3 ambient;
+uniform vec3 sunDir;
 
+uniform vec3 sunColor;
+
+uniform vec3 hasNormalMap;
+uniform vec3 hasSpecularMap;
+
 varying vec3 v_lighting;
 varying vec2 v_tex;
 varying vec4 v_shadow;
 varying vec2 v_los;
 varying vec2 v_blend;
+varying vec3 v_normal;
 
+varying vec3 v_half;
+
 #if USE_SPECULAR
   uniform float specularPower;
   uniform vec3 specularColor;
@@ -40,12 +51,12 @@
         vec4 size = vec4(offset + 1.0, 2.0 - offset);
         vec4 weight = (vec4(2.0 - 1.0 / size.xy, 1.0 / size.zw - 1.0) + (v_shadow.xy - offset).xyxy) * shadowScale.zwzw;
         return (1.0/9.0)*dot(size.zxzx*size.wwyy,
-          vec4(shadow2D(shadowTex, vec3(weight.zw, v_shadow.z)).a,
-               shadow2D(shadowTex, vec3(weight.xw, v_shadow.z)).a,
-               shadow2D(shadowTex, vec3(weight.zy, v_shadow.z)).a,
-               shadow2D(shadowTex, vec3(weight.xy, v_shadow.z)).a));
+          vec4(shadow2D(shadowTex, vec3(weight.zw, v_shadow.z)).r,
+               shadow2D(shadowTex, vec3(weight.xw, v_shadow.z)).r,
+               shadow2D(shadowTex, vec3(weight.zy, v_shadow.z)).r,
+               shadow2D(shadowTex, vec3(weight.xy, v_shadow.z)).r));
       #else
-        return shadow2D(shadowTex, v_shadow.xyz).a;
+        return shadow2D(shadowTex, v_shadow.xyz).r;
       #endif
     #else
       if (v_shadow.z >= 1.0)
@@ -83,8 +94,40 @@
     vec3 specular = vec3(0.0);
   #endif
 
-  vec3 color = (texdiffuse * sundiffuse + specular) * get_shadow() + texdiffuse * ambient;
+	vec3 bump = sundiffuse;
+	if (hasNormalMap.x > 0.5)
+	{
+		vec3 t = vec3(1, 0, 0);
+		t = normalize(t - v_normal * dot(v_normal, t));
+		vec3 b = cross(v_normal, t);
+		//vec3 b = vec3(0, 0, 1);
+		//b = normalize(b - v_normal * dot(v_normal, b));
 
+
+		mat3 tbn = transpose(mat3(t, b, v_normal));
+		vec3 ntex = texture2D(normTex, v_tex).rgb * 2.0 - 1.0;
+		vec3 vNout = normalize(ntex * tbn);
+
+
+		bump = vec3(max (dot (-sunDir, vNout), 0.0)) * sunColor;
+
+		vec4 specColour = vec4(1.0);
+
+		if (hasSpecularMap.x > 0.5)
+		{
+			specColour = texture2D(specTex, v_tex);
+			specular = specColour.xyz * sunColor * pow(max(0.0, dot(normalize(vNout), v_half)), 12);
+		}
+	}
+
+
+	//vec3 specular;
+
+	
+
+
+  vec3 color = (texdiffuse + specular) * bump * get_shadow() + texdiffuse * ambient;
+
   float los = texture2D(losTex, v_los).a;
   color *= los;
 
Index: binaries/data/mods/public/shaders/glsl/hdr.fs
===================================================================
--- binaries/data/mods/public/shaders/glsl/hdr.fs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/hdr.fs	(revision 0)
@@ -0,0 +1,64 @@
+#version 110
+
+varying vec2 v_tex;
+
+uniform sampler2D bgl_RenderedTexture;
+uniform sampler2D bgl_DepthTexture;
+uniform sampler2D bgl_LuminanceTexture;
+uniform float bgl_RenderedTextureWidth;
+uniform float bgl_RenderedTextureHeight;
+
+#define PI    3.14159265
+
+float width = bgl_RenderedTextureWidth; //texture width
+float height = bgl_RenderedTextureHeight; //texture height
+
+vec2 texCoord = v_tex;
+vec2 texcoord = v_tex;
+
+
+
+///float avgL = 0.38;
+//float HDRamount = 0.3;
+///float HDRamount = 0.1;
+
+//float avgL = 0.4;
+//float HDRamount = 0.1;
+
+//float avgL = 0.65;
+//float HDRamount = 0.1;
+
+float avgL = 0.7;
+float HDRamount = 0.1;
+
+void main(void)
+{
+	//gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+	//return;
+	
+
+	//avgL = int(texcoord.x * 10) / 10.0;
+	//HDRamount = int(texcoord.y * 10) / 10.0;
+
+	
+
+
+
+	float contrast = avgL;
+	float brightness = avgL * HDRamount;
+	
+	vec4 value =  texture2D(bgl_RenderedTexture, texcoord);
+
+	//value *= 0.5;
+	
+	gl_FragColor = (value/contrast)-brightness;
+
+	///float grey = (gl_FragColor.r + gl_FragColor.g + gl_FragColor.b) / 3.0;
+
+	///gl_FragColor = vec4(mix(gl_FragColor.rgb, vec3(grey), 0.2), 1.0);
+
+	//if (texcoord.x > 0.75) gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+
+}
+
+
Index: binaries/data/mods/public/shaders/glsl/model_common_norm.vs
===================================================================
--- binaries/data/mods/public/shaders/glsl/model_common_norm.vs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/model_common_norm.vs	(revision 0)
@@ -0,0 +1,110 @@
+#if USE_GPU_SKINNING
+// Skinning requires GLSL 1.30 for ivec4 vertex attributes
+#version 130
+#else
+#version 120
+#endif
+
+uniform mat4 transform;
+uniform vec3 cameraPos;
+uniform vec3 sunDir;
+uniform vec3 sunColor;
+uniform vec2 losTransform;
+uniform mat4 shadowTransform;
+uniform mat4 instancingTransform;
+
+//varying vec3 v_lighting;
+varying vec2 v_tex;
+varying vec4 v_shadow;
+varying vec2 v_los;
+
+//#if USE_SPECULAR
+  varying vec3 v_half;
+//#endif
+
+attribute vec3 a_vertex;
+attribute vec3 a_normal;
+attribute vec2 a_uv0;
+
+attribute vec4 a_tangent;
+
+
+
+#if USE_GPU_SKINNING
+  const int MAX_INFLUENCES = 4;
+  const int MAX_BONES = 64;
+  uniform mat4 skinBlendMatrices[MAX_BONES];
+  attribute ivec4 a_skinJoints;
+  attribute vec4 a_skinWeights;
+#endif
+
+varying vec4 debugx;
+
+
+
+
+//varying vec3 lightVec;
+varying vec3 eyeVec;
+
+varying vec3 v_normal;
+varying vec4 v_tangent;
+varying vec3 v_bitangent;
+
+
+//varying float sign;
+
+
+void main()
+{
+  #if USE_GPU_SKINNING
+    vec3 p = vec3(0.0);
+    vec3 n = vec3(0.0);
+    for (int i = 0; i < MAX_INFLUENCES; ++i) {
+      int joint = a_skinJoints[i];
+      if (joint != 0xff) {
+        mat4 m = skinBlendMatrices[joint];
+        p += vec3(m * vec4(a_vertex, 1.0)) * a_skinWeights[i];
+        n += vec3(m * vec4(a_normal, 0.0)) * a_skinWeights[i];
+      }
+    }
+    vec4 position = instancingTransform * vec4(p, 1.0);
+    vec3 normal = mat3(instancingTransform) * normalize(n);
+  #else
+  #if USE_INSTANCING
+    vec4 position = instancingTransform * vec4(a_vertex, 1.0);
+    vec3 normal = mat3(instancingTransform) * a_normal;
+    vec4 tangent = vec4(mat3(instancingTransform) * a_tangent.xyz, a_tangent.w);
+  #else
+    vec4 position = vec4(a_vertex, 1.0);
+    vec3 normal = a_normal;
+    vec4 tangent = a_tangent;    
+  #endif
+  #endif
+
+  gl_Position = transform * position;
+
+
+
+
+	eyeVec = cameraPos.xyz - position.xyz;
+	//lightVec = -sunDir;
+
+
+	
+ 
+	vec3 sunVec = -sunDir;
+    
+	v_half = normalize(sunVec + eyeVec);
+
+	
+	v_normal = normal;
+	v_tangent = tangent;//.xyz;
+	//sign = tangent.w;
+	v_bitangent = cross(v_normal, v_tangent);
+	
+
+  //v_lighting = max(0.0, dot(normal, -sunDir)) * sunColor;
+  v_tex = a_uv0;
+  v_shadow = shadowTransform * position;
+  v_los = position.xz * losTransform.x + losTransform.y;
+}
Index: binaries/data/mods/public/shaders/glsl/ssao.vs
===================================================================
--- binaries/data/mods/public/shaders/glsl/ssao.vs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/ssao.vs	(revision 0)
@@ -0,0 +1,37 @@
+#version 110
+
+uniform mat4 transform;
+
+varying vec2 v_tex;
+
+attribute vec3 a_vertex;
+attribute vec2 a_uv0;
+
+void main()
+{
+  //gl_Position = transform * vec4(a_vertex, 1.0);
+  gl_Position = gl_Vertex;  
+
+  v_tex = vec2(gl_MultiTexCoord0);
+}
+
+
+/*#version 110
+
+void main(void)
+{
+  gl_Position = gl_Vertex;  
+}
+
+//uniform mat4 transform;
+
+//attribute vec3 a_vertex;
+
+varying float  uv;
+
+
+void main()
+{
+  gl_Position = gl_Vertex;  
+}
+*/
\ No newline at end of file
Index: binaries/data/mods/public/shaders/glsl/terrain_base.xml
===================================================================
--- binaries/data/mods/public/shaders/glsl/terrain_base.xml	(revision 11919)
+++ binaries/data/mods/public/shaders/glsl/terrain_base.xml	(working copy)
@@ -6,6 +6,7 @@
         <stream name="color"/>
         <stream name="uv0"/>
         <attrib name="a_vertex" semantics="gl_Vertex"/>
+	<attrib name="a_normal" semantics="gl_Normal"/>
         <attrib name="a_color" semantics="gl_Color"/>
         <attrib name="a_uv0" semantics="gl_MultiTexCoord0"/>
     </vertex>
Index: binaries/data/mods/public/shaders/glsl/model_common_norm.xml
===================================================================
--- binaries/data/mods/public/shaders/glsl/model_common_norm.xml	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/model_common_norm.xml	(revision 0)
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<program type="glsl">
+
+    <vertex file="glsl/model_common_norm.vs">
+        <stream name="pos"/>
+        <stream name="normal"/>
+        <stream name="uv0"/>	
+        <attrib name="a_vertex" semantics="gl_Vertex"/>
+        <attrib name="a_normal" semantics="gl_Normal"/>
+        <attrib name="a_uv0" semantics="gl_MultiTexCoord0"/>	
+	<attrib name="a_tangent" semantics="CustomAttribute2"/>
+    </vertex>
+
+    <fragment file="glsl/model_common_norm.fs"/>
+
+</program>
Index: binaries/data/mods/public/shaders/glsl/terrain_common.vs
===================================================================
--- binaries/data/mods/public/shaders/glsl/terrain_common.vs	(revision 11919)
+++ binaries/data/mods/public/shaders/glsl/terrain_common.vs	(working copy)
@@ -17,13 +17,17 @@
 varying vec4 v_shadow;
 varying vec2 v_los;
 varying vec2 v_blend;
+varying vec3 v_normal;
 
+varying vec3 v_half;
+
 #if USE_SPECULAR
   varying vec3 v_normal;
   varying vec3 v_half;
 #endif
 
 attribute vec3 a_vertex;
+attribute vec3 a_normal;
 attribute vec3 a_color;
 attribute vec2 a_uv0;
 attribute vec2 a_uv1;
@@ -35,6 +39,10 @@
   gl_Position = transform * position;
 
   v_lighting = a_color * sunColor;
+
+  //v_lighting = dot(-sunDir, a_normal) * sunColor;
+
+  //v_lighting = sunColor;
   
   #if DECAL
     v_tex = a_uv0;
@@ -72,5 +80,14 @@
     v_normal = normal;
   #endif
 
+	vec3 eyeVec = cameraPos.xyz - position.xyz;
+	
+ 
+	vec3 sunVec = -sunDir;
+    
+	v_half = normalize(sunVec + eyeVec);
+
+  v_normal = a_normal;
+
   v_los = a_vertex.xz * losTransform.x + losTransform.yy;
 }
Index: binaries/data/mods/public/shaders/glsl/hdr.vs
===================================================================
--- binaries/data/mods/public/shaders/glsl/hdr.vs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/hdr.vs	(revision 0)
@@ -0,0 +1,37 @@
+#version 110
+
+uniform mat4 transform;
+
+varying vec2 v_tex;
+
+attribute vec3 a_vertex;
+attribute vec2 a_uv0;
+
+void main()
+{
+  //gl_Position = transform * vec4(a_vertex, 1.0);
+  gl_Position = gl_Vertex;  
+
+  v_tex = vec2(gl_MultiTexCoord0);
+}
+
+
+/*#version 110
+
+void main(void)
+{
+  gl_Position = gl_Vertex;  
+}
+
+//uniform mat4 transform;
+
+//attribute vec3 a_vertex;
+
+varying float  uv;
+
+
+void main()
+{
+  gl_Position = gl_Vertex;  
+}
+*/
\ No newline at end of file
Index: binaries/data/mods/public/shaders/glsl/ssao.xml
===================================================================
--- binaries/data/mods/public/shaders/glsl/ssao.xml	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/ssao.xml	(revision 0)
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<program type="glsl">
+
+    <vertex file="glsl/ssao.vs">
+        <stream name="pos"/>
+        <stream name="uv0"/>
+        <attrib name="a_vertex" semantics="gl_Vertex"/>
+        <attrib name="a_uv0" semantics="gl_MultiTexCoord0"/>
+    </vertex>
+
+    <fragment file="glsl/ssao.fs"/>
+
+</program>
Index: binaries/data/mods/public/shaders/glsl/terrain_blend.xml
===================================================================
--- binaries/data/mods/public/shaders/glsl/terrain_blend.xml	(revision 11919)
+++ binaries/data/mods/public/shaders/glsl/terrain_blend.xml	(working copy)
@@ -10,6 +10,7 @@
         <stream name="uv1"/>
         <attrib name="a_vertex" semantics="gl_Vertex"/>
         <attrib name="a_color" semantics="gl_Color"/>
+	<attrib name="a_normal" semantics="gl_Normal"/>
         <attrib name="a_uv0" semantics="gl_MultiTexCoord0"/>
         <attrib name="a_uv1" semantics="gl_MultiTexCoord1"/>
     </vertex>
Index: binaries/data/mods/public/shaders/glsl/bloom.xml
===================================================================
--- binaries/data/mods/public/shaders/glsl/bloom.xml	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/bloom.xml	(revision 0)
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<program type="glsl">
+
+    <vertex file="glsl/bloom.vs">
+        <stream name="pos"/>
+        <stream name="uv0"/>
+        <attrib name="a_vertex" semantics="gl_Vertex"/>
+        <attrib name="a_uv0" semantics="gl_MultiTexCoord0"/>
+    </vertex>
+
+    <fragment file="glsl/bloom.fs"/>
+
+</program>
Index: binaries/data/mods/public/shaders/glsl/hdr.xml
===================================================================
--- binaries/data/mods/public/shaders/glsl/hdr.xml	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/hdr.xml	(revision 0)
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<program type="glsl">
+
+    <vertex file="glsl/hdr.vs">
+        <stream name="pos"/>
+        <stream name="uv0"/>
+        <attrib name="a_vertex" semantics="gl_Vertex"/>
+        <attrib name="a_uv0" semantics="gl_MultiTexCoord0"/>
+    </vertex>
+
+    <fragment file="glsl/hdr.fs"/>
+
+</program>
Index: binaries/data/mods/public/shaders/glsl/bloom.fs
===================================================================
--- binaries/data/mods/public/shaders/glsl/bloom.fs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/bloom.fs	(revision 0)
@@ -0,0 +1,88 @@
+#version 110
+
+varying vec2 v_tex;
+
+uniform sampler2D bgl_RenderedTexture;
+uniform sampler2D bgl_DepthTexture;
+uniform sampler2D bgl_LuminanceTexture;
+uniform float bgl_RenderedTextureWidth;
+uniform float bgl_RenderedTextureHeight;
+
+#define PI    3.14159265
+
+float width = bgl_RenderedTextureWidth; //texture width
+float height = bgl_RenderedTextureHeight; //texture height
+
+vec2 texCoord = v_tex;
+vec2 texcoord = v_tex;
+
+
+//float BRIGHT_PASS_THRESHOLD = 0.3;
+//float BRIGHT_PASS_OFFSET = 0.5;
+
+float BRIGHT_PASS_THRESHOLD = 0.6;
+float BRIGHT_PASS_OFFSET = 0.6;
+
+#define blurclamp 0.0015
+#define bias 0.01
+
+#define KERNEL_SIZE  3.0
+
+
+
+vec4 bright(vec2 coo)
+{
+	//BRIGHT_PASS_THRESHOLD = int(texcoord.x * 10) / 10.0;
+	//BRIGHT_PASS_OFFSET = int(texcoord.y * 10) / 10.0;
+	
+	vec4 color = texture2D(bgl_RenderedTexture, coo);
+	
+	color = max(color - BRIGHT_PASS_THRESHOLD, 0.0);
+	
+	return color / (color + BRIGHT_PASS_OFFSET);	
+}
+
+
+void main(void)
+{	
+	//gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+	//return;
+
+
+	vec2 blur = vec2(clamp( bias, -blurclamp, blurclamp ));
+	
+	vec4 col = vec4( 0, 0, 0, 0 );
+	for ( float x = -KERNEL_SIZE + 1.0; x < KERNEL_SIZE; x += 1.0 )
+	{
+	for ( float y = -KERNEL_SIZE + 1.0; y < KERNEL_SIZE; y += 1.0 )
+	{
+		 col += bright( texcoord + vec2( blur.x * x, blur.y * y ) );
+	}
+	}
+	col /= ((KERNEL_SIZE+KERNEL_SIZE)-1.0)*((KERNEL_SIZE+KERNEL_SIZE)-1.0);
+	//gl_FragColor = col + texture2D(bgl_RenderedTexture, texcoord);
+
+	//col *= 0.9;
+	gl_FragColor = 1.0 - (1.0 - col) * (1.0 - texture2D(bgl_RenderedTexture, texcoord));
+
+
+	/*vec3 color = texture2D(bgl_RenderedTexture,texCoord).rgb;
+	vec3 luminance = texture2D(bgl_LuminanceTexture,texCoord).rgb;
+	vec3 white = vec3(1.0,1.0,1.0);
+	vec3 black = vec3(0.0,0.0,0.0);
+	//vec3 treshold = vec3(0.2,0.2,0.2);
+
+	vec3 treshold = vec3(0.3);
+	
+	luminance = clamp(max(black,luminance-treshold)+max(black,luminance-treshold)+max(black,luminance-treshold),0.0,1.0);
+	
+	if (texcoord.x > 0.5)
+	gl_FragColor = vec4(mix(gl_FragColor.rgb,color,1.0-luminance),1.0);*/
+	
+
+	//gl_FragColor = mix(gl_FragColor, texture2D(bgl_RenderedTexture, texcoord), 0.5);
+
+	//if (texcoord.x > 0.5) gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+
+}
+
Index: binaries/data/mods/public/shaders/glsl/model_common.fs
===================================================================
--- binaries/data/mods/public/shaders/glsl/model_common.fs	(revision 11919)
+++ binaries/data/mods/public/shaders/glsl/model_common.fs	(working copy)
@@ -47,12 +47,12 @@
         vec4 size = vec4(offset + 1.0, 2.0 - offset);
         vec4 weight = (vec4(2.0 - 1.0 / size.xy, 1.0 / size.zw - 1.0) + (v_shadow.xy - offset).xyxy) * shadowScale.zwzw;
         return (1.0/9.0)*dot(size.zxzx*size.wwyy,
-          vec4(shadow2D(shadowTex, vec3(weight.zw, v_shadow.z)).a,
-               shadow2D(shadowTex, vec3(weight.xw, v_shadow.z)).a,
-               shadow2D(shadowTex, vec3(weight.zy, v_shadow.z)).a,
-               shadow2D(shadowTex, vec3(weight.xy, v_shadow.z)).a));
+          vec4(shadow2D(shadowTex, vec3(weight.zw, v_shadow.z)).r,
+               shadow2D(shadowTex, vec3(weight.xw, v_shadow.z)).r,
+               shadow2D(shadowTex, vec3(weight.zy, v_shadow.z)).r,
+               shadow2D(shadowTex, vec3(weight.xy, v_shadow.z)).r));
       #else
-        return shadow2D(shadowTex, v_shadow.xyz).a;
+        return shadow2D(shadowTex, v_shadow.xyz).r;
       #endif
     #else
       if (v_shadow.z >= 1.0)
Index: binaries/data/mods/public/shaders/glsl/bloom.vs
===================================================================
--- binaries/data/mods/public/shaders/glsl/bloom.vs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/bloom.vs	(revision 0)
@@ -0,0 +1,37 @@
+#version 110
+
+uniform mat4 transform;
+
+varying vec2 v_tex;
+
+attribute vec3 a_vertex;
+attribute vec2 a_uv0;
+
+void main()
+{
+  //gl_Position = transform * vec4(a_vertex, 1.0);
+  gl_Position = gl_Vertex;  
+
+  v_tex = vec2(gl_MultiTexCoord0);
+}
+
+
+/*#version 110
+
+void main(void)
+{
+  gl_Position = gl_Vertex;  
+}
+
+//uniform mat4 transform;
+
+//attribute vec3 a_vertex;
+
+varying float  uv;
+
+
+void main()
+{
+  gl_Position = gl_Vertex;  
+}
+*/
\ No newline at end of file
Index: binaries/data/mods/public/shaders/glsl/model_common_norm.fs
===================================================================
--- binaries/data/mods/public/shaders/glsl/model_common_norm.fs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/model_common_norm.fs	(revision 0)
@@ -0,0 +1,212 @@
+#version 120
+
+uniform sampler2D baseTex;
+uniform sampler2D normTex;
+uniform sampler2D losTex;
+uniform sampler2D specTex;
+
+#if USE_SHADOW
+  #if USE_SHADOW_SAMPLER
+    uniform sampler2DShadow shadowTex;
+  #else
+    uniform sampler2D shadowTex;
+  #endif
+#endif
+
+#if USE_OBJECTCOLOR
+  uniform vec3 objectColor;
+#else
+#if USE_PLAYERCOLOR
+  uniform vec3 playerColor;
+#endif
+#endif
+
+uniform vec3 ambient;
+uniform vec4 shadowOffsets1;
+uniform vec4 shadowOffsets2;
+
+//uniform mat4 normalMatrix;
+
+//varying vec3 v_lighting;
+varying vec2 v_tex;
+varying vec4 v_shadow;
+varying vec2 v_los;
+
+uniform vec3 sunDir;
+uniform vec3 sunColor;
+
+/*#if USE_SPECULAR
+  uniform float specularPower;
+  uniform vec3 specularColor;
+  varying vec3 v_normal;
+#endif*/
+
+varying vec3 v_half;
+
+//varying vec3 lightVec;	
+varying vec3 eyeVec;
+varying vec3 v_normal;
+varying vec4 v_tangent;
+varying vec3 v_bitangent;
+
+
+//varying float sign;
+
+
+float get_shadow()
+{
+  #if USE_SHADOW && !DISABLE_RECEIVE_SHADOWS
+    #if USE_SHADOW_SAMPLER
+      #if USE_SHADOW_PCF
+        return 0.25 * (
+          shadow2D(shadowTex, vec3(v_shadow.xy + shadowOffsets1.xy, v_shadow.z)).r +
+          shadow2D(shadowTex, vec3(v_shadow.xy + shadowOffsets1.zw, v_shadow.z)).r +
+          shadow2D(shadowTex, vec3(v_shadow.xy + shadowOffsets2.xy, v_shadow.z)).r +
+          shadow2D(shadowTex, vec3(v_shadow.xy + shadowOffsets2.zw, v_shadow.z)).r
+        );
+      #else
+        return shadow2D(shadowTex, v_shadow.xyz).r;
+      #endif
+    #else
+      if (v_shadow.z >= 1.0)
+        return 1.0;
+      #if USE_SHADOW_PCF
+        return (
+          (v_shadow.z <= texture2D(shadowTex, v_shadow.xy + shadowOffsets1.xy).x ? 0.25 : 0.0) +
+          (v_shadow.z <= texture2D(shadowTex, v_shadow.xy + shadowOffsets1.zw).x ? 0.25 : 0.0) +
+          (v_shadow.z <= texture2D(shadowTex, v_shadow.xy + shadowOffsets2.xy).x ? 0.25 : 0.0) +
+          (v_shadow.z <= texture2D(shadowTex, v_shadow.xy + shadowOffsets2.zw).x ? 0.25 : 0.0)
+        );
+      #else
+        return (v_shadow.z <= texture2D(shadowTex, v_shadow.xy).x ? 1.0 : 0.0);
+      #endif
+    #endif
+  #else
+    return 1.0;
+  #endif
+}
+
+
+#define USE_PARALLAX      1
+#define USE_NORMAL_MAP    1
+#define USE_SPECULAR_MAP  1
+#define USE_SELF_LIGHT    1
+
+void main()
+{
+
+
+	vec3 color;
+	mat3 tbn;
+	vec2 coord = v_tex;
+	float sign = v_tangent.w;
+
+
+#if USE_PARALLAX
+	float h = texture2D(normTex, coord).a;
+
+	tbn = transpose(mat3(v_tangent.xyz, v_bitangent * sign, v_normal));
+
+	vec3 eyeDir = normalize( tbn * eyeVec );
+	float dist = length(eyeVec);
+
+	if (dist < 100 && h < 0.999)
+	{
+		float scale = 0.01;
+
+		if (dist > 65)
+		{
+			scale = 0.01 * (100 - dist) / 35.0;
+		}
+
+		float height = 1.0;
+		float iter;
+
+		float s;
+		vec2 move;
+		iter = (75 - (dist - 25)) / 3 + 5;
+
+		if (iter > 15) iter = 15;
+
+		vec3 tangEye = -eyeDir;
+
+		iter = mix(iter * 2, iter, tangEye.z);
+		
+		s = 1.0 / iter;
+		move = vec2(-eyeDir.x, eyeDir.y) * scale / (eyeDir.z * iter);
+
+		while (h < height) 
+		{
+			height -= s;
+			coord += move;
+			h = texture2D(normTex, coord).a;
+		}
+
+	}
+#endif
+	
+
+
+	tbn = transpose(mat3(v_tangent.xyz, v_bitangent * -sign, v_normal));
+	vec3 ntex = texture2D(normTex, coord).rgb * 2.0 - 1.0;
+	vec3 vNout = normalize(ntex * tbn);
+
+
+
+#if USE_NORMAL_MAP
+	float bump = max (dot (-sunDir, vNout), 0.0) ;
+#else
+	float bump = 1.0;
+#endif
+
+
+	vec3 specular;
+	vec4 specColour = vec4(1.0);
+#if USE_SPECULAR_MAP
+	specColour = texture2D(specTex, coord);
+	specular = bump * specColour.xyz * sunColor * pow(max(0.0, dot(normalize(vNout), v_half)), 12);
+#else
+	// if enabled, do normal specular
+	specular = vec3(0.0);
+#endif
+	
+
+	vec4 diffuse = texture2D(baseTex, coord);
+
+#if USE_TRANSPARENT
+	gl_FragColor.a = diffuse.a;
+#else
+	gl_FragColor.a = 1.0;
+#endif
+
+	vec3 texdiffuse = diffuse.rgb;
+
+	// Apply-coloring based on texture alpha
+#if USE_OBJECTCOLOR
+	texdiffuse *= mix(objectColor, vec3(1.0, 1.0, 1.0), diffuse.a);
+#else
+#if USE_PLAYERCOLOR
+	texdiffuse *= mix(playerColor, vec3(1.0, 1.0, 1.0), diffuse.a);
+#endif
+#endif
+
+
+
+
+	color = (texdiffuse + specular) * bump * sunColor * get_shadow() + texdiffuse * ambient;
+
+#if USE_SELF_LIGHT
+	color = mix(texdiffuse, color, specColour.a);
+#endif
+
+	
+
+
+#if !IGNORE_LOS
+	float los = texture2D(losTex, v_los).a;
+	color *= los;
+#endif
+
+  
+	gl_FragColor.rgb = color;
+}
Index: binaries/data/mods/public/shaders/glsl/ssao.fs
===================================================================
--- binaries/data/mods/public/shaders/glsl/ssao.fs	(revision 0)
+++ binaries/data/mods/public/shaders/glsl/ssao.fs	(revision 0)
@@ -0,0 +1,324 @@
+#version 110
+
+varying vec2 v_tex;
+
+uniform sampler2D bgl_RenderedTexture;
+uniform sampler2D bgl_DepthTexture;
+uniform sampler2D bgl_LuminanceTexture;
+uniform float bgl_RenderedTextureWidth;
+uniform float bgl_RenderedTextureHeight;
+
+#define PI    3.14159265
+
+float width = bgl_RenderedTextureWidth; //texture width
+float height = bgl_RenderedTextureHeight; //texture height
+
+//float near = 10; //Z-near
+//float far = 100; //Z-far
+
+float near = 0.5; //Z-near
+float far = 75.0; //Z-far
+
+int samples = 6; //samples on the first ring (5-10)
+int rings = 3; //ring count (2-6)
+
+//vec2 texCoord = gl_TexCoord[0].st;
+vec2 texCoord = v_tex;
+vec2 texcoord = v_tex;
+
+
+vec2 rand(in vec2 coord) //generating random noise
+{
+	float noiseX = (fract(sin(dot(coord ,vec2(12.9898,78.233))) * 43758.5453));
+	float noiseY = (fract(sin(dot(coord ,vec2(12.9898,78.233)*2.0)) * 43758.5453));
+	return vec2(noiseX,noiseY);
+}
+
+float readDepth(in vec2 coord) 
+{
+	return (2.0 * near) / (far + near - texture2D(bgl_DepthTexture, coord ).x * (far-near)); 	
+}
+
+float compareDepths( in float depth1, in float depth2 )
+{
+	float aoCap = 1.2;
+	float aoMultiplier = 250.0;
+	float depthTolerance = 0.0000;
+	float aorange = 2.5;// units in space the AO effect extends to (this gets divided by the camera far range
+	float diff = sqrt(clamp(1.0-(depth1-depth2) / (aorange/(far-near)),0.0,1.0));
+	float ao = min(aoCap,max(0.0,depth1-depth2-depthTolerance) * aoMultiplier) * diff;
+	return ao;
+}
+
+void main(void)
+{	
+	/*if (texture2D(bgl_DepthTexture, texcoord).x <= 0.5) 
+	{	
+	    gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+	    return;
+	}*/
+
+	float depth = readDepth(texCoord);
+	float d;
+	
+	float aspect = width/height;
+	vec2 noise = rand(texCoord)*0.003;
+	
+	float w = (0.001/depth+noise.x);
+	float h = (0.001/depth+noise.y);
+	
+	float pw;
+	float ph;
+
+	float ao;	
+	float s;
+	
+	for (int i = 0 ; i < rings; ++i)
+	{
+		for (int j = 0 ; j < samples; ++j)
+		{
+			float step = PI*2.0 / float(samples);
+			pw = (cos(float(j)*step)*float(i));
+			ph = (sin(float(j)*step)*float(i))*aspect;
+			d = readDepth( vec2(texCoord.s+pw*w,texCoord.t+ph*h));
+			ao += compareDepths(depth,d);	
+			s += 1.0;
+		}
+	}
+	
+	ao /= s;
+	ao = 1.0-ao;	
+    
+	vec3 color = texture2D(bgl_RenderedTexture,texCoord).rgb;
+	//vec3 luminance = texture2D(bgl_LuminanceTexture,texCoord).rgb;
+	vec3 luminance = vec3(dot(color, vec3(0.299, 0.587, 0.114)));
+	vec3 white = vec3(1.0,1.0,1.0);
+	vec3 black = vec3(0.0,0.0,0.0);
+	//vec3 treshold = vec3(0.2,0.2,0.2);
+
+	vec3 treshold = vec3(0.8);
+	
+	luminance = clamp(max(black,luminance-treshold)+max(black,luminance-treshold)+max(black,luminance-treshold),0.0,1.0);
+	
+	gl_FragColor = vec4(color*mix(vec3(ao,ao,ao),white,luminance),1.0);
+	//gl_FragColor = vec4(mix(vec3(ao,ao,ao),white,luminance),1.0);// vec4(color*vec3(ao,ao,ao),1.0);
+	//gl_FragColor = vec4(mix(vec3(ao,ao,ao),white,luminance),1.0);
+	//gl_FragColor = vec4(ao * color, 1.0);
+
+	//if (texcoord.x > 0.75) gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+}
+
+#if (0)
+
+vec2 rand(in vec2 coord) //generating random noise
+{
+	float noiseX = (fract(sin(dot(coord ,vec2(12.9898,78.233))) * 43758.5453));
+	float noiseY = (fract(sin(dot(coord ,vec2(12.9898,78.233)*2.0)) * 43758.5453));
+	return vec2(noiseX,noiseY);
+}
+
+float readDepth(in vec2 coord) 
+{
+	return (2.0 * near) / (far + near - texture2D(bgl_DepthTexture, coord ).x * (far-near)); 	
+}
+
+float compareDepths( in float depth1, in float depth2 )
+{
+	float aoCap = 1.0;
+	float aoMultiplier = 100.0;
+	float depthTolerance = 0.0000;
+	float aorange = 6.0;// units in space the AO effect extends to (this gets divided by the camera far range
+	float diff = sqrt(clamp(1.0-(depth1-depth2) / (aorange/(far-near)),0.0,1.0));
+	float ao = min(aoCap,max(0.0,depth1-depth2-depthTolerance) * aoMultiplier) * diff;
+	return ao;
+}
+
+void main(void)
+{	
+	float depth = readDepth(texCoord);
+	float d;
+	
+	float aspect = width/height;
+	vec2 noise = rand(texCoord)*0.003;
+	
+	float w = (0.001/depth+noise.x);
+	float h = (0.001/depth+noise.y);
+	
+	float pw;
+	float ph;
+
+	float ao;	
+	float s;
+	
+	for (int i = 0 ; i < rings; ++i)
+	{
+		for (int j = 0 ; j < samples; ++j)
+		{
+			float step = PI*2.0 / float(samples);
+			pw = (cos(float(j)*step)*float(i));
+			ph = (sin(float(j)*step)*float(i))*aspect;
+			d = readDepth( vec2(texCoord.s+pw*w,texCoord.t+ph*h));
+			ao += compareDepths(depth,d);	
+			s += 1.0;
+		}
+	}
+	
+	ao /= s;
+	ao = 1.0-ao;	
+    
+	vec3 color = texture2D(bgl_RenderedTexture,texCoord).rgb;
+	vec3 luminance = texture2D(bgl_LuminanceTexture,texCoord).rgb;
+	vec3 white = vec3(1.0,1.0,1.0);
+	vec3 black = vec3(0.0,0.0,0.0);
+	vec3 treshold = vec3(0.2,0.2,0.2);
+	
+	luminance = clamp(max(black,luminance-treshold)+max(black,luminance-treshold)+max(black,luminance-treshold),0.0,1.0);
+	
+	gl_FragColor = vec4(color*mix(vec3(ao,ao,ao),white,luminance), 1.0); //vec4(color*mix(vec3(ao,ao,ao),white,luminance),1.0);
+
+	//gl_FragColor = vec4(color*mix(vec3(ao,ao,ao),white,luminance),1.0);
+	//gl_FragColor = ao * texture2D(bgl_RenderedTexture, texCoord );
+	//gl_FragColor = ao;
+
+	//gl_FragColor = texture2D(bgl_RenderedTexture, texCoord);
+
+	if (texcoord.x > 0.5) gl_FragColor = texture2D(bgl_RenderedTexture, texcoord);
+}
+
+#endif
+
+/*#define camx 100
+#define camy 200
+
+
+int j;
+int i;
+
+
+
+float readDepth( in vec2 coord ) {
+ return (2.0 * camx) / (camy + camx - texture2D(bgl_DepthTexture, coord ).x * (camy - camx)); 	
+}
+
+float compareDepths( in float depth1, in float depth2 ) {
+	float aoCap = 1.0;
+	float aoMultiplier=100.0;
+	float depthTolerance=0.0000;
+	float aorange = 20.0;// units in space the AO effect extends to (this gets divided by the camera far range
+	float diff = sqrt( clamp(1.0-(depth1-depth2) / (aorange/(camy-camx)),0.0,1.0) );
+	float ao = min(aoCap,max(0.0,depth1-depth2-depthTolerance) * aoMultiplier) * diff;
+	return ao;
+}
+
+void main(void)
+{	
+	//gl_FragColor = texture2D(bgl_RenderedTexture, texCoord);
+
+	float depth = readDepth( texCoord );
+	float d;
+	
+	float pw = 1.0 / float(width);
+	float ph = 1.0 / float(height);
+
+	float aoCap = 2.0;
+
+	float ao = 5.0;
+	
+	float aoMultiplier=1.0;
+
+	float depthTolerance = 0.0000;
+	
+	float aoscale=0.7;
+	
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+	
+	pw*=2.0;
+	ph*=2.0;
+	aoMultiplier/=2.0;
+	aoscale*=1.2;
+	
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	pw*=2.0;
+	ph*=2.0;
+	aoMultiplier/=2.0;
+	aoscale*=1.4;
+	
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+	
+	pw*=2.0;
+	ph*=2.0;
+	aoMultiplier/=2.0;
+	aoscale*=1.6;
+	
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+	
+	pw*=2.0;
+	ph*=2.0;
+	aoMultiplier/=2.0;
+	aoscale*=1.8;
+	
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t+ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s+pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+
+	d=readDepth( vec2(texCoord.s-pw,texCoord.t-ph));
+	ao+=compareDepths(depth,d)/aoscale;
+	
+	ao/=15.0;
+	
+    vec4 finalao = vec4(1.0-ao,1.0-ao,1.0-ao,1.0); 
+	finalao = clamp(finalao,0.0,0.7)+0.3;	
+    
+	gl_FragColor = finalao * texture2D(bgl_RenderedTexture, texCoord );
+	gl_FragColor.a = 1.0;  
+
+
+	gl_FragColor = texture2D(bgl_RenderedTexture, texCoord);
+}
+*/
\ No newline at end of file
Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg	(revision 11919)
+++ binaries/data/config/default.cfg	(working copy)
@@ -58,7 +58,7 @@
 renderpath = default
 
 ; Prefer GLSL shaders over ARB shaders (not recommended)
-preferglsl = false
+preferglsl = true
 
 ; Replace alpha-blending with alpha-testing, for performance experiments
 forcealphatest = false
