diff --git a/Source/Core/Data/DataManager.cs b/Source/Core/Data/DataManager.cs
index d3d921e66bfab64f5e2139acc153f83ba25001d4..4bd180cd0fa60316dbe25f246e6b7059468f73eb 100755
--- a/Source/Core/Data/DataManager.cs
+++ b/Source/Core/Data/DataManager.cs
@@ -481,9 +481,6 @@ namespace CodeImp.DoomBuilder.Data
 			LoadSndSeq();
 			LoadSndInfo();
 			LoadVoxels();
-			Dictionary<string, int> actorsbyclass = CreateActorsByClassList();
-			LoadModeldefs(actorsbyclass);
-			foreach(Thing t in General.Map.Map.Things) t.UpdateCache();
 			General.MainWindow.DisplayReady();
 			
 			// Process colormaps (we just put them in as textures)
@@ -568,7 +565,6 @@ namespace CodeImp.DoomBuilder.Data
 
 			//mxd. Should be done after loading textures...
 			int hirestexcount = LoadHiResTextures();
-			LoadGldefs(actorsbyclass);
 
 			//mxd. Create camera textures. Should be done after loading textures.
 			LoadAnimdefs();
@@ -580,8 +576,15 @@ namespace CodeImp.DoomBuilder.Data
 			texturenames.Sort();
 			flatnames.Sort();
 
+			// biwa. Moved model processing after texture processing, since the model might need one of those textures
+			Dictionary<string, int> actorsbyclass = CreateActorsByClassList();
+			LoadModeldefs(actorsbyclass);
+			foreach (Thing t in General.Map.Map.Things) t.UpdateCache();
+
+			LoadGldefs(actorsbyclass);
+
 			// Sort things
-			foreach(ThingCategory tc in thingcategories) tc.SortIfNeeded();
+			foreach (ThingCategory tc in thingcategories) tc.SortIfNeeded();
 
 			// Update the used textures
 			General.Map.Data.UpdateUsedTextures();
diff --git a/Source/Core/GZBuilder/Data/ModelData.cs b/Source/Core/GZBuilder/Data/ModelData.cs
index d5b6c7af07de694cf6cfbefc41c67f53c45961f7..52b0d01f85bb0fa9885729b6f3bea39e224d2a62 100755
--- a/Source/Core/GZBuilder/Data/ModelData.cs
+++ b/Source/Core/GZBuilder/Data/ModelData.cs
@@ -14,7 +14,9 @@ namespace CodeImp.DoomBuilder.GZBuilder.Data
 	{
 		#region ================== Constants
 
-		public static readonly string[] SUPPORTED_TEXTURE_EXTENSIONS = { ".jpg", ".tga", ".png", ".dds", ".pcx" };
+		// Keep sage order of extensions as in GZDoom's r_data\models\models.cpp FindGFXFile function. That doesn't
+		// list .dds, but just keep it in here
+		public static readonly string[] SUPPORTED_TEXTURE_EXTENSIONS = { ".png", ".jpg", ".tga", ".pcx", ".dds" };
 
 		#endregion
 
diff --git a/Source/Core/GZBuilder/md3/ModelReader.cs b/Source/Core/GZBuilder/md3/ModelReader.cs
index 90d374b6cb28b82d3cd788dc31923faf34d791be..ec2a7aba15c55594fcc96bdfe4fd902b0889b013 100755
--- a/Source/Core/GZBuilder/md3/ModelReader.cs
+++ b/Source/Core/GZBuilder/md3/ModelReader.cs
@@ -4,6 +4,7 @@ using System;
 using System.IO;
 using System.Drawing;
 using System.Drawing.Imaging;
+using System.Globalization; // biwa
 using System.Text;
 using System.Collections.Generic;
 using CodeImp.DoomBuilder.IO;
@@ -128,6 +129,15 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
                     case ".3d":
                         result = Read3DModel(ref bbs, skins, ms, device, mde.FrameIndices[i], mde.ModelNames[i], containers);
                         break;
+					case ".obj":
+						// OBJ doesn't support frames, so print out an error
+						if (mde.FrameIndices[i] > 0)
+						{
+							General.ErrorLogger.Add(ErrorType.Error, "Trying to load frame " + mde.FrameIndices[i] + " of model \"" + mde.ModelNames[i] + "\", but OBJ doesn't support frames!");
+							continue;
+						}
+						result = ReadOBJModel(ref bbs, skins, ms, device, mde.ModelNames[i]);
+						break;
 					default:
 						result.Errors = "model format is not supported";
 						break;
@@ -160,28 +170,22 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
 						//try to use model's own skins
 						for(int m = 0; m < result.Meshes.Count; m++)
 						{
-							if(string.IsNullOrEmpty(result.Skins[m]))
+							// biwa. Makes sure to add a dummy texture if the MODELDEF skin definition is erroneous
+							if(m >= result.Skins.Count)
 							{
+								errors.Add("no skin defined for mesh " + m + ".");
 								mde.Model.Textures.Add(General.Map.Data.UnknownTexture3D.Texture);
-								errors.Add("texture not found in MODELDEF or model skin.");
 								continue;
 							}
 
-							string path = result.Skins[m].Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
-							ext = Path.GetExtension(path);
-
-							if(Array.IndexOf(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, ext) == -1)
+							if(string.IsNullOrEmpty(result.Skins[m]))
 							{
 								mde.Model.Textures.Add(General.Map.Data.UnknownTexture3D.Texture);
-								errors.Add("image format \"" + ext + "\" is not supported!");
+								errors.Add("texture not found in MODELDEF or model skin.");
 								continue;
 							}
 
-							//relative path?
-							if(path.IndexOf(Path.DirectorySeparatorChar) == -1)
-								path = Path.Combine(Path.GetDirectoryName(mde.ModelNames[i]), path);
-
-							Texture t = LoadTexture(containers, path, device);
+							Texture t = GetTexture(containers, result.Skins[m], device);
 
 							if(t == null)
 							{
@@ -196,11 +200,12 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
 					//Try to use texture loaded from MODELDEFS
 					else
 					{
-						Texture t = LoadTexture(containers, mde.SkinNames[i], device);
+						Texture t = GetTexture(containers, mde.SkinNames[i], device);
+
 						if(t == null)
 						{
 							mde.Model.Textures.Add(General.Map.Data.UnknownTexture3D.Texture);
-							errors.Add("unable to load texture \"" + mde.SkinNames[i] + "\"");
+							errors.Add("unable to load skin \"" + mde.SkinNames[i] + "\"");
 						}
 						else
 						{
@@ -237,6 +242,92 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
 			mde.Model.Radius = Math.Max(Math.Max(Math.Abs(bbs.MinY), Math.Abs(bbs.MaxY)), Math.Max(Math.Abs(bbs.MinX), Math.Abs(bbs.MaxX)));
 		}
 
+		private static Texture GetTexture(List<DataReader> containers, string texturename, Device device)
+		{
+			Texture t = null;
+			string[] extensions = new string[ModelData.SUPPORTED_TEXTURE_EXTENSIONS.Length + 1];
+
+			Array.Copy(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, 0, extensions, 1, ModelData.SUPPORTED_TEXTURE_EXTENSIONS.Length);
+			extensions[0] = "";
+
+			// Try to load the texture as defined by its path. GZDoom doesn't care about extensions
+			if (t == null)
+			{
+				foreach (string extension in extensions)
+				{
+					string name = Path.ChangeExtension(texturename, null) + extension;
+					name = name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+
+					t = LoadTexture(containers, name, device);
+
+					if (t != null)
+						break;
+				}
+			}
+
+			// Try to use an already defined texture. Again, just try out all extensions
+			foreach (string extension in extensions)
+			{
+				string name = Path.ChangeExtension(texturename, null) + extension;
+
+				if (General.Map.Data.GetTextureExists(name))
+				{
+					ImageData image = General.Map.Data.GetTextureImage(name);
+
+					if (!image.IsImageLoaded)
+						image.LoadImage();
+
+					if (image.Texture == null)
+						image.CreateTexture();
+
+					t = image.Texture;
+
+					break;
+				}
+			}
+
+			// GZDoom can also ignore the path completely (because why not), so let's see if there's a texture with
+			// just the skin name
+			if (t == null)
+			{
+				string name = Path.ChangeExtension(Path.GetFileName(texturename), null);
+
+				if (General.Map.Data.GetTextureExists(name))
+				{
+					ImageData image = General.Map.Data.GetTextureImage(name);
+
+					if (!image.IsImageLoaded)
+						image.LoadImage();
+
+					if (image.Texture == null)
+						image.CreateTexture();
+
+					t = image.Texture;
+				}
+			}
+
+			// Or maybe it's a sprite
+			if(t == null)
+			{
+				string name = Path.ChangeExtension(texturename, null);
+
+				if (General.Map.Data.GetSpriteExists(name))
+				{
+					ImageData image = General.Map.Data.GetSpriteImage(name);
+
+					if (!image.IsImageLoaded)
+						image.LoadImage();
+
+					if (image.Texture == null)
+						image.CreateTexture();
+
+					t = image.Texture;
+				}
+			}
+
+			return t;
+		}
+
         #endregion
 
         #region ================== 3D (unreal)
@@ -1225,6 +1316,322 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
 
 		#endregion
 
+		#region ================== OBJ
+
+		private static MD3LoadResult ReadOBJModel(ref BoundingBoxSizes bbs, Dictionary<int, string> skins, Stream s, Device device, string name)
+		{
+			MD3LoadResult result = new MD3LoadResult();
+
+			using (var reader = new StreamReader(s, Encoding.ASCII))
+			{
+				string line;
+				int linenum = 1;
+				string message;
+				int surfaceskinid = 0;
+				List<Vector3D> vertices = new List<Vector3D>();
+				List<int> faces = new List<int>();
+				List<Vector3D> normals = new List<Vector3D>();
+				List<Vector2D> texcoords = new List<Vector2D>();
+				List<WorldVertex> worldvertices = new List<WorldVertex>();
+				List<int> polyindiceslist = new List<int>();
+
+				while ((line = reader.ReadLine()) != null) {
+					string[] fields = line.Trim().Split(new[] { ' ', '\t' }, 2, StringSplitOptions.RemoveEmptyEntries);
+
+					if (fields.Length == 0) continue; // Empty line
+					if (fields[0].Trim() == "#") continue; // Comment
+
+					string keyword = fields[0].Trim();
+					string payload = fields[1].Trim();
+
+					switch(keyword)
+					{
+						case "v":
+							Vector3D v = new Vector3D(0, 0, 0);
+
+							if (OBJParseVertex(payload, ref v, out message))
+								vertices.Add(v);
+							else
+							{
+								result.Errors = String.Format("Error in line {0}: {1}", linenum, message);
+								return result;
+							}
+
+							break;
+						case "vt":
+							Vector2D t = new Vector2D(0, 0);
+
+							if (OBJParseTextureCoords(payload, ref t, out message))
+								texcoords.Add(t);
+							else
+							{
+								result.Errors = String.Format("Error in line {0}: {1}", linenum, message);
+								return result;
+							}
+
+							break;
+						case "vn":
+							Vector3D n = new Vector3D(0, 0, 0);
+
+							if (OBJParseNormal(payload, ref n, out message))
+								normals.Add(n);
+							else
+							{
+								result.Errors = String.Format("Error in line {0}: {1}", linenum, message);
+								return result;
+							}
+
+							break;
+						case "f":
+							List<int> fv = new List<int>();
+							List<int> vt = new List<int>();
+							List<int> vn = new List<int>();
+
+							if (OBJParseFace(payload, ref fv, ref vt, ref vn, out message))
+							{
+								// Sanity check for vertices
+								for (int i=0; i < fv.Count; i++)
+									if(fv[i] != -1 && fv[i] > vertices.Count)
+									{
+										result.Errors = String.Format("Error in line {0}: vertex {1} does not exist", linenum, i + 1);
+										return result;
+									}
+
+								// Sanity check for texture coordinates
+								for (int i=0; i < vt.Count; i++)
+									if(vt[i] != -1 && vt[i] > texcoords.Count)
+									{
+										result.Errors = String.Format("Error in line {0}: texture coordinate {1} does not exist", linenum, i + 1);
+										return result;
+									}
+
+								// Sanity check for normals
+								for (int i = 0; i < vn.Count; i++)
+									if (vn[i] != -1 && vn[i] > normals.Count)
+									{
+										result.Errors = String.Format("Error in line {0}: vertex {1} does not exist", linenum, i + 1);
+										return result;
+									}
+
+								int[] seq;
+
+								// If the face is a quad split it into two triangles
+								if (fv.Count == 3)
+									seq = new int[] { 0, 1, 2 };
+								else
+									seq = new int[] { 0, 1, 2, 0, 2, 3 };
+
+								for (int i = 0; i < seq.Length; i++) {
+									WorldVertex wc = new WorldVertex(vertices[fv[seq[i]]]);
+
+									if(vt[seq[i]] != -1)
+									{
+										wc.u = texcoords[vt[seq[i]]].x;
+										wc.v = texcoords[vt[seq[i]]].y;
+									}
+
+									if (vn[seq[i]] != -1)
+									{
+										wc.nx = normals[vn[seq[i]]].x;
+										wc.ny = normals[vn[seq[i]]].y;
+										wc.nz = normals[vn[seq[i]]].z;
+									}
+
+									BoundingBoxTools.UpdateBoundingBoxSizes(ref bbs, wc);
+
+									worldvertices.Add(wc);
+									polyindiceslist.Add(polyindiceslist.Count);
+								}
+							}
+							else
+							{
+								result.Errors = String.Format("Error in line {0}: {1}", linenum, message);
+								return result;
+							}
+
+							break;
+						case "usemtl":
+							// If there's a new texture defined create a mesh from the current faces and
+							// start a gather new faces for the next mesh
+							if(worldvertices.Count > 0)
+							{
+								CreateMesh(device, ref result, worldvertices, polyindiceslist);
+								worldvertices.Clear();
+								polyindiceslist.Clear();
+							}
+							if (fields.Length >= 2)
+								result.Skins.Add(fields[1]);
+							
+							surfaceskinid++;
+							break;
+						case "": // Empty line
+						case "#": // Line is a comment
+						case "s": // Smooth
+						default:
+							break;
+					}
+
+					linenum++;
+				}
+
+				CreateMesh(device, ref result, worldvertices, polyindiceslist);
+
+				// Overwrite internal textures with SurfaceSkin definitions if necessary
+				if (skins != null)
+				{
+					foreach (KeyValuePair<int, string> group in skins)
+					{
+						// Add dummy skins if necessary
+						while (result.Skins.Count <= group.Key)
+							result.Skins.Add(String.Empty);
+
+						result.Skins[group.Key] = group.Value;
+					}
+				}
+			}
+
+			return result;
+		}
+
+		private static bool OBJParseVertex(string payload, ref Vector3D v, out string message)
+		{
+			string[] fields = payload.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+			if(fields.Length < 3)
+			{
+				message = "too few arguments";
+				return false;
+			}
+
+			try
+			{
+				v.x = float.Parse(fields[0], CultureInfo.InvariantCulture);
+				v.z = float.Parse(fields[1], CultureInfo.InvariantCulture);
+				v.y = float.Parse(fields[2], CultureInfo.InvariantCulture);
+
+
+				// Prepare to fix rotation angle
+				float angleOfsetCos = (float)Math.Cos(Angle2D.PIHALF);
+				float angleOfsetSin = (float)Math.Sin(Angle2D.PIHALF);
+
+				// Fix rotation angle
+				float rx = angleOfsetCos * v.x - angleOfsetSin * v.y;
+				float ry = angleOfsetSin * v.x + angleOfsetCos * v.y;
+				v.x = rx;
+				v.y = ry;
+			}
+			catch (FormatException e)
+			{
+				message = "field is not a float";
+				return false;
+			}
+
+			message = "";
+			return true;
+		}
+
+		private static bool OBJParseTextureCoords(string payload, ref Vector2D t, out string message)
+		{
+			string[] fields = payload.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+			if (fields.Length < 2)
+			{
+				message = "too few arguments";
+				return false;
+			}
+
+			try
+			{
+				t.x = float.Parse(fields[0], CultureInfo.InvariantCulture);
+
+				if (fields.Length == 2)
+					t.y = float.Parse(fields[1], CultureInfo.InvariantCulture);
+				else
+					t.y = 0.0f;
+			}
+			catch (FormatException e)
+			{
+				message = "field is not a float";
+				return false;
+			}
+
+			message = "";
+			return true;
+		}
+
+		private static bool OBJParseNormal(string payload, ref Vector3D normal, out string message)
+		{
+			string[] fields = payload.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+			if (fields.Length < 3)
+			{
+				message = "too few arguments";
+				return false;
+			}
+
+			try
+			{
+				normal.x = float.Parse(fields[0], CultureInfo.InvariantCulture);
+				normal.y = float.Parse(fields[1], CultureInfo.InvariantCulture);
+				normal.z = float.Parse(fields[2], CultureInfo.InvariantCulture);
+			}
+			catch (FormatException e)
+			{
+				message = "field is not a float";
+				return false;
+			}
+
+			message = "";
+			return true;
+		}
+
+		private static bool OBJParseFace(string payload, ref List<int> face, ref List<int> texcoords, ref List<int> normals, out string message)
+		{
+			string[] fields = payload.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+			if (fields.Length < 3)
+			{
+				message = "too few arguments";
+				return false;
+			}
+
+			if(fields.Length > 4)
+			{
+				message = "faces with more than 4 sides are not supported";
+				return false;
+			}
+
+			try
+			{
+				for (int i = 0; i < fields.Length; i++)
+				{
+					string[] vertexdata = fields[i].Split('/');
+
+					face.Add(int.Parse(vertexdata[0], CultureInfo.InvariantCulture) - 1);
+
+					if (vertexdata.Length >= 1 && vertexdata[1] != "")
+						texcoords.Add(int.Parse(vertexdata[1], CultureInfo.InvariantCulture) - 1);
+					else
+						texcoords.Add(-1);
+
+					if (vertexdata.Length >= 2 && vertexdata[2] != "")
+						normals.Add(int.Parse(vertexdata[2], CultureInfo.InvariantCulture) - 1);
+					else
+						normals.Add(-1);
+				}
+			}
+			catch(FormatException e)
+			{
+				message = "field is not an integer";
+				return false;
+			}
+
+			message = "";
+			return true;
+		}
+
+		#endregion
+
 		#region ================== Utility
 
 		private static MemoryStream LoadFile(List<DataReader> containers, string path, bool isModel)
@@ -1243,7 +1650,7 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
 		{
 			if(string.IsNullOrEmpty(path)) return null;
 
-			MemoryStream ms = LoadFile(containers, path, false);
+			MemoryStream ms = LoadFile(containers, path, true);
 			if(ms == null) return null;
 
 			Texture texture = null;
diff --git a/Source/Core/ZDoom/ModeldefStructure.cs b/Source/Core/ZDoom/ModeldefStructure.cs
index f2d527035a482a9c658497a08e6b59280a73d669..af171e2fa099a3048976a42fadf9a757ee0fe283 100755
--- a/Source/Core/ZDoom/ModeldefStructure.cs
+++ b/Source/Core/ZDoom/ModeldefStructure.cs
@@ -150,9 +150,9 @@ namespace CodeImp.DoomBuilder.ZDoom
 							return false;
 						}
 
-						if(modelext != ".md3" && modelext != ".md2" && modelext != ".3d") 
+						if(modelext != ".md3" && modelext != ".md2" && modelext != ".3d" && modelext != ".obj") 
 						{
-							parser.ReportError("Model \"" + token + "\" won't be loaded. Only Unreal 3D, MD2 and MD3 models are supported");
+							parser.ReportError("Model \"" + token + "\" won't be loaded. Only Unreal 3D, MD2, MD3, and OBJ models are supported");
 							return false;
 						}
 
@@ -193,16 +193,8 @@ namespace CodeImp.DoomBuilder.ZDoom
 						// Check invalid path chars
 						if(!parser.CheckInvalidPathChars(token)) return false;
 
-						// Check extension
-						string texext = Path.GetExtension(token);
-						if(Array.IndexOf(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, texext) == -1) 
-						{
-							parser.ReportError("Image format \"" + texext + "\" is not supported");
-							return false;
-						} 
-
 						// GZDoom allows skins with identical index, it uses the last one encountered
-						skinnames[skinindex] = Path.Combine(path, token).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+						skinnames[skinindex] = Path.Combine(path, token);
 						break;
 
 					// SurfaceSkin <int modelindex> <int surfaceindex> <string skinfile>
@@ -258,14 +250,6 @@ namespace CodeImp.DoomBuilder.ZDoom
 						// Check invalid path chars
 						if(!parser.CheckInvalidPathChars(token)) return false;
 
-						// Check extension
-						string skinext = Path.GetExtension(token);
-						if(Array.IndexOf(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, skinext) == -1)
-						{
-							parser.ReportError("Image format \"" + skinext + "\" is not supported");
-							return false;
-						} 
-
 						// Store
 						surfaceskinenames[modelindex][surfaceindex] = Path.Combine(path, token).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
 						break;