From dde8742ff20b53403429285b680482faa6e9acca Mon Sep 17 00:00:00 2001 From: biwa Date: Sat, 15 Jun 2019 17:44:02 +0200 Subject: [PATCH] - Added support for Wavefront OBJ models - Fixed a problem where model textures were not loaded from the textures directory or from TEXTUREx definitions. Fixes #173 and #226 --- Source/Core/Data/DataManager.cs | 13 +- Source/Core/GZBuilder/Data/ModelData.cs | 4 +- Source/Core/GZBuilder/md3/ModelReader.cs | 443 ++++++++++++++++++++++- Source/Core/ZDoom/ModeldefStructure.cs | 22 +- 4 files changed, 439 insertions(+), 43 deletions(-) diff --git a/Source/Core/Data/DataManager.cs b/Source/Core/Data/DataManager.cs index d3d921e6..4bd180cd 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 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 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 d5b6c7af..52b0d01f 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 90d374b6..ec2a7aba 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,6 +170,14 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3 //try to use model's own skins for(int m = 0; m < result.Meshes.Count; 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); + continue; + } + if(string.IsNullOrEmpty(result.Skins[m])) { mde.Model.Textures.Add(General.Map.Data.UnknownTexture3D.Texture); @@ -167,21 +185,7 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3 continue; } - string path = result.Skins[m].Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - ext = Path.GetExtension(path); - - if(Array.IndexOf(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, ext) == -1) - { - mde.Model.Textures.Add(General.Map.Data.UnknownTexture3D.Texture); - errors.Add("image format \"" + ext + "\" is not supported!"); - 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 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 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 vertices = new List(); + List faces = new List(); + List normals = new List(); + List texcoords = new List(); + List worldvertices = new List(); + List polyindiceslist = new List(); + + 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 fv = new List(); + List vt = new List(); + List vn = new List(); + + 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 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 face, ref List texcoords, ref List 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 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 f2d52703..af171e2f 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 @@ -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;