diff --git a/components/nif/property.hpp b/components/nif/property.hpp
index cd1e0a5d11..00cdc0e008 100644
--- a/components/nif/property.hpp
+++ b/components/nif/property.hpp
@@ -109,6 +109,17 @@ public:
      * 5 - Bump map texture
      * 6 - Decal texture
      */
+    enum TextureType
+    {
+        BaseTexture = 0,
+        DarkTexture = 1,
+        DetailTexture = 2,
+        GlossTexture = 3,
+        GlowTexture = 4,
+        BumpTexture = 5,
+        DecalTexture = 6
+    };
+
     Texture textures[7];
 
     void read(NIFStream *nif)
diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp
index 89f55fbd4e..c5dc7fbb98 100644
--- a/components/nifogre/ogrenifloader.cpp
+++ b/components/nifogre/ogrenifloader.cpp
@@ -555,11 +555,14 @@ static std::string findTextureName(const std::string &filename)
      * texture file name references were kept as .tga.
      */
     static const char path[] = "textures\\";
+    static const char path2[] = "textures/";
+
 
     std::string texname = filename;
     Misc::StringUtils::toLower(texname);
 
-    if(texname.compare(0, sizeof(path)-1, path) != 0)
+    if(texname.compare(0, sizeof(path)-1, path) != 0
+            && texname.compare(0, sizeof(path2)-1, path2) != 0)
         texname = path + texname;
 
     Ogre::String::size_type pos = texname.rfind('.');
@@ -575,7 +578,8 @@ static std::string findTextureName(const std::string &filename)
         {
             texname = filename;
             Misc::StringUtils::toLower(texname);
-            if(texname.compare(0, sizeof(path)-1, path) != 0)
+            if(texname.compare(0, sizeof(path)-1, path) != 0
+                    && texname.compare(0, sizeof(path2)-1, path2) != 0)
                 texname = path + texname;
         }
     }
@@ -590,7 +594,8 @@ static Ogre::String getMaterial(const Nif::NiTriShape *shape, const Ogre::String
                                 const Nif::NiAlphaProperty *alphaprop,
                                 const Nif::NiVertexColorProperty *vertprop,
                                 const Nif::NiZBufferProperty *zprop,
-                                const Nif::NiSpecularProperty *specprop)
+                                const Nif::NiSpecularProperty *specprop,
+                                bool &needTangents)
 {
     Ogre::MaterialManager &matMgr = Ogre::MaterialManager::getSingleton();
     Ogre::MaterialPtr material = matMgr.getByName(name);
@@ -634,6 +639,7 @@ static Ogre::String getMaterial(const Nif::NiTriShape *shape, const Ogre::String
                 warn("Found internal texture, ignoring.");
         }
     }
+    needTangents = !texName[Nif::NiTexturingProperty::BumpTexture].empty();
 
     // Alpha modifiers
     if(alphaprop)
@@ -741,7 +747,8 @@ static Ogre::String getMaterial(const Nif::NiTriShape *shape, const Ogre::String
             new sh::Vector4(specular.x, specular.y, specular.z, glossiness)));
     }
 
-    instance->setProperty("diffuseMap", sh::makeProperty(texName[0]));
+    instance->setProperty("diffuseMap", sh::makeProperty(texName[Nif::NiTexturingProperty::BaseTexture]));
+    instance->setProperty("normalMap", sh::makeProperty(texName[Nif::NiTexturingProperty::BumpTexture]));
     for(int i = 1;i < 7;i++)
     {
         if(!texName[i].empty())
@@ -1022,11 +1029,20 @@ class NIFMeshLoader : Ogre::ManualResourceLoader
             }
         }
 
+        bool needTangents=false;
         std::string matname = NIFMaterialLoader::getMaterial(shape, mesh->getName(), mGroup,
                                                              texprop, matprop, alphaprop,
-                                                             vertprop, zprop, specprop);
+                                                             vertprop, zprop, specprop, needTangents);
         if(matname.length() > 0)
             sub->setMaterialName(matname);
+
+        // build tangents if the material needs them
+        if (needTangents)
+        {
+            unsigned short src,dest;
+            if (!mesh->suggestTangentVectorBuildParams(Ogre::VES_TANGENT, src,dest))
+                mesh->buildTangentVectors(Ogre::VES_TANGENT, src,dest);
+        }
     }
 
     bool findTriShape(Ogre::Mesh *mesh, const Nif::Node *node,
diff --git a/extern/shiny/Docs/Macros.dox b/extern/shiny/Docs/Macros.dox
index 0578c447f3..c04ebd3747 100644
--- a/extern/shiny/Docs/Macros.dox
+++ b/extern/shiny/Docs/Macros.dox
@@ -107,6 +107,23 @@
 
 	\section properties Property retrieval / binding
 
+	\subsection shPropertyHasValue shPropertyHasValue
+	
+	Usage: \@shPropertyHasValue(property)
+
+	Gets replaced by 1 if the property's value is not empty, or 0 if it is empty.
+	Useful for checking whether an optional texture is present or not.
+	
+	Example:
+	\code	
+	#if @shPropertyHasValue(specularMap)
+	 // specular mapping code
+	#endif
+	#if @shPropertyHasValue(normalMap)
+	 // normal mapping code
+	#endif
+	\endcode
+
 	\subsection shUniformProperty shUniformProperty
 
 	Usage: \@shUniformProperty<4f|3f|2f|1f|int> (uniformName, property)
@@ -130,15 +147,11 @@
 
 	Example:
 	\code
-	#if @shPropertyBool(has_normal_map)
+	#if @shPropertyBool(has_vertex_colors)
 	...
 	#endif
 	\endcode
 
-	\subsection shPropertyNotBool shPropertyNotBool
-
-	Same as shPropertyBool, but inverts the result (i.e. when shPropertyBool would return 0, this returns 1 and vice versa)
-
 	\subsection shPropertyString shPropertyString
 
 	Retrieve a string property of the pass that this shader belongs to
diff --git a/extern/shiny/Main/MaterialInstance.hpp b/extern/shiny/Main/MaterialInstance.hpp
index 000f9d60c9..36ba37ddba 100644
--- a/extern/shiny/Main/MaterialInstance.hpp
+++ b/extern/shiny/Main/MaterialInstance.hpp
@@ -25,6 +25,7 @@ namespace sh
 	public:
 		virtual void requestedConfiguration (MaterialInstance* m, const std::string& configuration) = 0; ///< called before creating
 		virtual void createdConfiguration (MaterialInstance* m, const std::string& configuration) = 0; ///< called after creating
+		virtual ~MaterialInstanceListener(){}
 	};
 
 	/**
diff --git a/extern/shiny/Main/ShaderInstance.cpp b/extern/shiny/Main/ShaderInstance.cpp
index 1539128aba..b44c63c322 100644
--- a/extern/shiny/Main/ShaderInstance.cpp
+++ b/extern/shiny/Main/ShaderInstance.cpp
@@ -194,13 +194,6 @@ namespace sh
 					bool val = retrieveValue(value, properties->getContext()).get();
 					replaceValue = val ? "1" : "0";
 				}
-				else if (cmd == "shPropertyNotBool") // same as above, but inverts the result
-				{
-					std::string propertyName = args[0];
-					PropertyValuePtr value = properties->getProperty(propertyName);
-					bool val = retrieveValue(value, properties->getContext()).get();
-					replaceValue = val ? "0" : "1";
-				}
 				else if (cmd == "shPropertyString")
 				{
 					std::string propertyName = args[0];
@@ -214,6 +207,14 @@ namespace sh
 					std::string value = retrieveValue(properties->getProperty(propertyName), properties->getContext()).get();
 					replaceValue = (value == comparedAgainst) ? "1" : "0";
 				}
+				else if (isCmd(source, pos, "@shPropertyHasValue"))
+				{
+					assert(args.size() == 1);
+					std::string propertyName = args[0];
+					PropertyValuePtr value = properties->getProperty(propertyName);
+					std::string val = retrieveValue(value, properties->getContext()).get();
+					replaceValue = (val.empty() ? "0" : "1");
+				}
 				else
 					throw std::runtime_error ("unknown command \"" + cmd + "\"");
 				source.replace(pos, (end+1)-pos, replaceValue);
diff --git a/extern/shiny/Main/ShaderSet.cpp b/extern/shiny/Main/ShaderSet.cpp
index 413d7d1a26..628e0acee7 100644
--- a/extern/shiny/Main/ShaderSet.cpp
+++ b/extern/shiny/Main/ShaderSet.cpp
@@ -6,6 +6,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include "Factory.hpp"
 
@@ -26,6 +27,10 @@ namespace sh
 		std::ifstream stream(sourceFile.c_str(), std::ifstream::in);
 		std::stringstream buffer;
 
+		boost::filesystem::path p (sourceFile);
+		p = p.branch_path();
+		mBasePath = p.string();
+
 		buffer << stream.rdbuf();
 		stream.close();
 		mSource = buffer.str();
@@ -52,6 +57,12 @@ namespace sh
 						size_t start = currentToken.find('(')+1;
 						mGlobalSettings.push_back(currentToken.substr(start, currentToken.find(')')-start));
 					}
+					else if (boost::starts_with(currentToken, "@shPropertyHasValue"))
+					{
+						assert ((currentToken.find('(') != std::string::npos) && (currentToken.find(')') != std::string::npos));
+						size_t start = currentToken.find('(')+1;
+						mPropertiesToExist.push_back(currentToken.substr(start, currentToken.find(')')-start));
+					}
 					else if (boost::starts_with(currentToken, "@shPropertyEqual"))
 					{
 						assert ((currentToken.find('(') != std::string::npos) && (currentToken.find(')') != std::string::npos)
@@ -135,6 +146,11 @@ namespace sh
 		{
 			boost::hash_combine(seed, retrieveValue(currentGlobalSettings->getProperty(*it), NULL).get());
 		}
+		for (std::vector::iterator it = mPropertiesToExist.begin(); it != mPropertiesToExist.end(); ++it)
+		{
+			std::string v = retrieveValue(properties->getProperty(*it), properties->getContext()).get();
+			boost::hash_combine(seed, static_cast(v != ""));
+		}
 		boost::hash_combine(seed, static_cast(Factory::getInstance().getCurrentLanguage()));
 		return seed;
 	}
diff --git a/extern/shiny/Main/ShaderSet.hpp b/extern/shiny/Main/ShaderSet.hpp
index a423b6779f..b21278ac99 100644
--- a/extern/shiny/Main/ShaderSet.hpp
+++ b/extern/shiny/Main/ShaderSet.hpp
@@ -53,6 +53,10 @@ namespace sh
 		std::vector  mGlobalSettings; ///< names of the global settings that affect the shader source
 		std::vector  mProperties; ///< names of the per-material properties that affect the shader source
 
+		std::vector  mPropertiesToExist;
+		///< same as mProperties, however in this case, it is only relevant if the property is empty or not
+		/// (we don't care about the value)
+
 		ShaderInstanceMap mInstances; ///< maps permutation ID (generated from the properties) to \a ShaderInstance
 
 		void parse(); ///< find out which properties and global settings affect the shader source
diff --git a/files/materials/objects.mat b/files/materials/objects.mat
index 5e18a666a7..00b43a9d0d 100644
--- a/files/materials/objects.mat
+++ b/files/materials/objects.mat
@@ -6,6 +6,7 @@ material openmw_objects_base
     emissive 0.0 0.0 0.0
     vertmode 0
     diffuseMap black.png
+    normalMap
 
     is_transparent false // real transparency, alpha rejection doesn't count here
     scene_blend default
@@ -22,6 +23,7 @@ material openmw_objects_base
         {
             vertexcolor_mode $vertmode
             is_transparent $is_transparent
+            normalMap $normalMap
         }
 
         diffuse $diffuse
@@ -38,6 +40,11 @@ material openmw_objects_base
             direct_texture $diffuseMap
             create_in_ffp true
         }
+
+        texture_unit normalMap
+        {
+            direct_texture $normalMap
+        }
       
         texture_unit shadowMap0
         {
diff --git a/files/materials/objects.shader b/files/materials/objects.shader
index c8616e9d1e..73968a6b3e 100644
--- a/files/materials/objects.shader
+++ b/files/materials/objects.shader
@@ -14,13 +14,15 @@
 #define NEED_DEPTH
 #endif
 
+#define NORMAL_MAP @shPropertyHasValue(normalMap)
+
+// if normal mapping is enabled, we force pixel lighting
+#define VERTEX_LIGHTING (!@shPropertyHasValue(normalMap))
 
 #define UNDERWATER @shGlobalSettingBool(render_refraction)
 
 #define VERTEXCOLOR_MODE @shPropertyString(vertexcolor_mode)
 
-#define VERTEX_LIGHTING 1
-
 #define VIEWPROJ_FIX @shGlobalSettingBool(viewproj_fix)
 
 #ifdef SH_VERTEX_SHADER
@@ -42,6 +44,16 @@
         shVertexInput(float2, uv0)
         shOutput(float2, UV)
         shNormalInput(float4)
+
+#if NORMAL_MAP
+        shTangentInput(float4)
+        shOutput(float3, tangentPassthrough)
+#endif
+
+#if !VERTEX_LIGHTING
+        shOutput(float3, normalPassthrough)
+#endif
+
 #ifdef NEED_DEPTH
         shOutput(float, depthPassthrough)
 #endif
@@ -52,6 +64,10 @@
         shColourInput(float4)
 #endif
 
+#if VERTEXCOLOR_MODE != 0 && !VERTEX_LIGHTING
+        shOutput(float4, colourPassthrough)
+#endif
+
 #if VERTEX_LIGHTING
     shUniform(float, lightCount) @shAutoConstant(lightCount, light_count)
     shUniform(float4, lightPosition[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightPosition, light_position_view_space_array, @shGlobalSettingString(num_lights))
@@ -94,6 +110,15 @@
     {
 	    shOutputPosition = shMatrixMult(wvp, shInputPosition);
 	    UV = uv0;
+#if NORMAL_MAP
+        tangentPassthrough = tangent.xyz;
+#endif
+#if !VERTEX_LIGHTING
+        normalPassthrough = normal.xyz;
+#endif
+#if VERTEXCOLOR_MODE != 0 && !VERTEX_LIGHTING
+        colourPassthrough = colour;
+#endif
 
 #ifdef NEED_DEPTH
 
@@ -188,15 +213,31 @@
 #endif
 
     SH_BEGIN_PROGRAM
-		shSampler2D(diffuseMap)
-		shInput(float2, UV)
+        shSampler2D(diffuseMap)
+
+#if NORMAL_MAP
+        shSampler2D(normalMap)
+#endif
+
+        shInput(float2, UV)
+
+#if NORMAL_MAP
+        shInput(float3, tangentPassthrough)
+#endif
+#if !VERTEX_LIGHTING
+        shInput(float3, normalPassthrough)
+#endif
 
 #ifdef NEED_DEPTH
         shInput(float, depthPassthrough)
 #endif
 
         shInput(float3, objSpacePositionPassthrough)
-        
+
+#if VERTEXCOLOR_MODE != 0 && !VERTEX_LIGHTING
+        shInput(float4, colourPassthrough)
+#endif
+
 #if FOG
         shUniform(float3, fogColour) @shAutoConstant(fogColour, fog_colour)
         shUniform(float4, fogParams) @shAutoConstant(fogParams, fog_params)
@@ -222,23 +263,98 @@
 
 #if (UNDERWATER) || (FOG)
         shUniform(float4x4, worldMatrix) @shAutoConstant(worldMatrix, world_matrix)
-        shUniform(float4, cameraPos) @shAutoConstant(cameraPos, camera_position) 
+        shUniform(float4, cameraPos) @shAutoConstant(cameraPos, camera_position)
 #endif
 
 #if UNDERWATER
-        shUniform(float, waterLevel) @shSharedParameter(waterLevel) 
+        shUniform(float, waterLevel) @shSharedParameter(waterLevel)
         shUniform(float, waterEnabled) @shSharedParameter(waterEnabled)
 #endif
 
 #if VERTEX_LIGHTING
     shInput(float4, lightResult)
     shInput(float3, directionalResult)
+#else
+    shUniform(float, lightCount) @shAutoConstant(lightCount, light_count)
+    shUniform(float4, lightPosition[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightPosition, light_position_view_space_array, @shGlobalSettingString(num_lights))
+    shUniform(float4, lightDiffuse[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightDiffuse, light_diffuse_colour_array, @shGlobalSettingString(num_lights))
+    shUniform(float4, lightAttenuation[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightAttenuation, light_attenuation_array, @shGlobalSettingString(num_lights))
+    shUniform(float4, lightAmbient)                    @shAutoConstant(lightAmbient, ambient_light_colour)
+    shUniform(float4x4, worldView) @shAutoConstant(worldView, worldview_matrix)
+    #if VERTEXCOLOR_MODE != 2
+    shUniform(float4, materialAmbient)                    @shAutoConstant(materialAmbient, surface_ambient_colour)
+    #endif
+    #if VERTEXCOLOR_MODE != 2
+    shUniform(float4, materialDiffuse)                    @shAutoConstant(materialDiffuse, surface_diffuse_colour)
+    #endif
+    #if VERTEXCOLOR_MODE != 1
+    shUniform(float4, materialEmissive)                   @shAutoConstant(materialEmissive, surface_emissive_colour)
+    #endif
 #endif
 
     SH_START_PROGRAM
     {
         shOutputColour(0) = shSample(diffuseMap, UV);
-            
+
+#if NORMAL_MAP
+        float3 normal = normalPassthrough;
+        float3 binormal = cross(tangentPassthrough.xyz, normal.xyz);
+        float3x3 tbn = float3x3(tangentPassthrough.xyz, binormal, normal.xyz);
+
+        #if SH_GLSL
+            tbn = transpose(tbn);
+        #endif
+
+        float3 TSnormal = shSample(normalMap, UV.xy).xyz * 2 - 1;
+
+        normal = normalize (shMatrixMult( transpose(tbn), TSnormal ));
+#endif
+
+#if !VERTEX_LIGHTING
+        float3 viewPos = shMatrixMult(worldView, float4(objSpacePositionPassthrough,1)).xyz;
+        float3 viewNormal = normalize(shMatrixMult(worldView, float4(normal.xyz, 0)).xyz);
+
+        float3 lightDir;
+        float d;
+        float4 lightResult = float4(0,0,0,1);
+        @shForeach(@shGlobalSettingString(num_lights))
+            lightDir = lightPosition[@shIterator].xyz - (viewPos * lightPosition[@shIterator].w);
+            d = length(lightDir);
+            lightDir = normalize(lightDir);
+
+#if VERTEXCOLOR_MODE == 2
+            lightResult.xyz += colourPassthrough.xyz * lightDiffuse[@shIterator].xyz
+                    * shSaturate(1.0 / ((lightAttenuation[@shIterator].y) + (lightAttenuation[@shIterator].z * d) + (lightAttenuation[@shIterator].w * d * d)))
+                    * max(dot(viewNormal.xyz, lightDir), 0);
+#else
+            lightResult.xyz += materialDiffuse.xyz * lightDiffuse[@shIterator].xyz
+                    * shSaturate(1.0 / ((lightAttenuation[@shIterator].y) + (lightAttenuation[@shIterator].z * d) + (lightAttenuation[@shIterator].w * d * d)))
+                    * max(dot(viewNormal.xyz, lightDir), 0);
+#endif
+
+#if @shIterator == 0
+            float3 directionalResult = lightResult.xyz;
+#endif
+
+        @shEndForeach
+
+
+#if VERTEXCOLOR_MODE == 2
+        lightResult.xyz += lightAmbient.xyz * colourPassthrough.xyz + materialEmissive.xyz;
+        lightResult.a *= colourPassthrough.a;
+#endif
+#if VERTEXCOLOR_MODE == 1
+        lightResult.xyz += lightAmbient.xyz * materialAmbient.xyz + colourPassthrough.xyz;
+#endif
+#if VERTEXCOLOR_MODE == 0
+        lightResult.xyz += lightAmbient.xyz * materialAmbient.xyz + materialEmissive.xyz;
+#endif
+
+#if VERTEXCOLOR_MODE != 2
+        lightResult.a *= materialDiffuse.a;
+#endif
+#endif
+
             // shadows only for the first (directional) light
 #if SHADOWS
             float shadow = depthShadowPCF (shadowMap0, lightSpacePos0, invShadowmapSize0);
@@ -275,7 +391,7 @@
 
 #if FOG
         float fogValue = shSaturate((depthPassthrough - fogParams.y) * fogParams.w);
-        
+
 
 #if UNDERWATER
         shOutputColour(0).xyz = shLerp (shOutputColour(0).xyz, UNDERWATER_COLOUR, shSaturate(length(waterEyePos-worldPos) / VISIBILITY));