diff --git a/Crash.UI/Properties/Resources.Designer.cs b/Crash.UI/Properties/Resources.Designer.cs index afe29e40..dd5a9284 100755 --- a/Crash.UI/Properties/Resources.Designer.cs +++ b/Crash.UI/Properties/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -393,6 +392,15 @@ public static string NSFController_AcAddWavebankChunk { } } + /// + /// Looks up a localized string similar to Export all scenery to OBJ. + /// + public static string NSFController_AcExportScenery { + get { + return ResourceManager.GetString("NSFController_AcExportScenery", resourceCulture); + } + } + /// /// Looks up a localized string similar to Fix Box Count. /// diff --git a/Crash.UI/Properties/Resources.resx b/Crash.UI/Properties/Resources.resx index fe32bd03..17f04671 100755 --- a/Crash.UI/Properties/Resources.resx +++ b/Crash.UI/Properties/Resources.resx @@ -357,4 +357,7 @@ Show All Level Zones + + Export all scenery to OBJ + \ No newline at end of file diff --git a/Crash/Formats/Crash Formats/Animation/OldFrame.cs b/Crash/Formats/Crash Formats/Animation/OldFrame.cs old mode 100755 new mode 100644 index 0b960428..c0dc6909 --- a/Crash/Formats/Crash Formats/Animation/OldFrame.cs +++ b/Crash/Formats/Crash Formats/Animation/OldFrame.cs @@ -133,46 +133,5 @@ public byte[] Save() } return data; } - - public byte[] ToOBJ(OldModelEntry model) - { - long xorigin = 0; - long yorigin = 0; - long zorigin = 0; - //foreach (OldFrameVertex vertex in vertices) - //{ - // xorigin += vertex.X; - // yorigin += vertex.Y; - // zorigin += vertex.Z; - //} - //xorigin /= vertices.Count; - //yorigin /= vertices.Count; - //zorigin /= vertices.Count; - xorigin -= XOffset; - yorigin -= YOffset; - zorigin -= ZOffset; - using (MemoryStream stream = new MemoryStream()) - { - using (StreamWriter obj = new StreamWriter(stream)) - { - obj.WriteLine("# Vertices"); - foreach (OldFrameVertex vertex in vertices) - { - obj.WriteLine("v {0} {1} {2}", vertex.X - xorigin, vertex.Y - yorigin, vertex.Z - zorigin); - } - foreach (OldFrameVertex vertex in vertices) - { - obj.WriteLine("vn {0} {1} {2}", vertex.NormalX / 127.0, vertex.NormalY / 127.0, vertex.NormalZ / 127.0); - } - obj.WriteLine(); - obj.WriteLine("# Polygons"); - foreach (OldModelPolygon polygon in model.Polygons) - { - obj.WriteLine("f {0}//{0} {1}//{1} {2}//{2}", polygon.VertexA / 6 + 1, polygon.VertexB / 6 + 1, polygon.VertexC / 6 + 1); - } - } - return stream.ToArray(); - } - } } } diff --git a/Crash/Formats/Crash Formats/Scenery/SceneryEntry.cs b/Crash/Formats/Crash Formats/Scenery/SceneryEntry.cs old mode 100755 new mode 100644 index 85a8c075..62a4e645 --- a/Crash/Formats/Crash Formats/Scenery/SceneryEntry.cs +++ b/Crash/Formats/Crash Formats/Scenery/SceneryEntry.cs @@ -104,242 +104,5 @@ public override UnprocessedEntry Unprocess() } return new UnprocessedEntry(items, EID, Type); } - - public byte[] ToOBJ() - { - using (MemoryStream stream = new MemoryStream()) - { - using (StreamWriter obj = new StreamWriter(stream)) - { - obj.WriteLine("# Vertices"); - foreach (SceneryVertex vertex in vertices) - { - if (vertex.Color < Colors.Count) - { - SceneryColor color = Colors[vertex.Color]; - obj.WriteLine("v {0} {1} {2} {3} {4} {5}", vertex.X + XOffset / 16, vertex.Y + YOffset / 16, vertex.Z + ZOffset / 16, color.Red / 255.0, color.Green / 255.0, color.Blue / 255.0); - } - else - { - obj.WriteLine("v {0} {1} {2}", vertex.X + XOffset / 16, vertex.Y + YOffset / 16, vertex.Z + ZOffset / 16); - } - } - obj.WriteLine(); - obj.WriteLine("# Triangles"); - foreach (SceneryTriangle triangle in triangles) - { - obj.WriteLine("f {0} {1} {2}", triangle.VertexA + 1, triangle.VertexB + 1, triangle.VertexC + 1); - } - obj.WriteLine(); - obj.WriteLine("# Quads"); - foreach (SceneryQuad quad in quads) - { - obj.WriteLine("f {0} {1} {2} {3}", quad.VertexA + 1, quad.VertexB + 1, quad.VertexC + 1, quad.VertexD + 1); - } - } - return stream.ToArray(); - } - } - - public byte[] ToPLY() - { - using (MemoryStream stream = new MemoryStream()) - { - using (StreamWriter ply = new StreamWriter(stream)) - { - int polycount = 0; - foreach (SceneryTriangle triangle in triangles) - { - if (triangle.VertexA < vertices.Count - 1 && triangle.VertexB < vertices.Count - 1 && triangle.VertexC < vertices.Count - 1) - { - ++polycount; - } - } - foreach (SceneryQuad quad in quads) - { - if (quad.VertexA < vertices.Count - 1 && quad.VertexB < vertices.Count - 1 && quad.VertexC < vertices.Count - 1 && quad.VertexD < vertices.Count - 1) - { - ++polycount; - } - } - ply.WriteLine("ply"); - ply.WriteLine("format ascii 1.0"); - ply.WriteLine("element vertex {0}", Vertices.Count); - ply.WriteLine("property int x"); - ply.WriteLine("property int y"); - ply.WriteLine("property int z"); - ply.WriteLine("property uchar red"); - ply.WriteLine("property uchar green"); - ply.WriteLine("property uchar blue"); - ply.WriteLine("element face {0}", polycount); - ply.WriteLine("property list uchar int vertex_index"); - ply.WriteLine("end_header"); - foreach (SceneryVertex vertex in vertices) - { - if (vertex.Color < Colors.Count) - { - SceneryColor color = Colors[vertex.Color]; - ply.WriteLine("{0} {1} {2} {3} {4} {5}", vertex.X + XOffset / 16, vertex.Y + YOffset / 16, vertex.Z + ZOffset / 16, color.Red, color.Green, color.Blue); - } - else - { - ply.WriteLine("{0} {1} {2} 255 0 255", vertex.X + XOffset / 16, vertex.Y + YOffset / 16, vertex.Z + ZOffset / 16); - } - } - foreach (SceneryTriangle triangle in triangles) - { - if (triangle.VertexA < vertices.Count - 1 && triangle.VertexB < vertices.Count - 1 && triangle.VertexC < vertices.Count - 1) - { - ply.WriteLine("3 {0} {1} {2}", triangle.VertexA, triangle.VertexB, triangle.VertexC); - } - } - foreach (SceneryQuad quad in quads) - { - if (quad.VertexA < vertices.Count - 1 && quad.VertexB < vertices.Count - 1 && quad.VertexC < vertices.Count - 1 && quad.VertexD < vertices.Count - 1) - { - ply.WriteLine("4 {0} {1} {2} {3}", quad.VertexA, quad.VertexB, quad.VertexC, quad.VertexD); - } - } - } - return stream.ToArray(); - } - } - - /*public byte[] ToCOLLADA() - { - using (MemoryStream stream = new MemoryStream()) - { - using (StreamWriter textwriter = new StreamWriter(stream)) - using (XmlTextWriter xmlwriter = new XmlTextWriter(textwriter)) - { - xmlwriter.WriteStartDocument(); - xmlwriter.WriteStartElement("COLLADA"); - xmlwriter.WriteAttributeString("xmlns", "http://www.collada.org/2005/11/COLLADASchema"); - xmlwriter.WriteAttributeString("version", "1.4.1"); - xmlwriter.WriteStartElement("library_geometries"); - xmlwriter.WriteStartElement("geometry"); - xmlwriter.WriteStartElement("mesh"); - xmlwriter.WriteStartElement("source"); - xmlwriter.WriteAttributeString("id", "positions"); - xmlwriter.WriteStartElement("float_array"); - xmlwriter.WriteAttributeString("id", "positions-array"); - xmlwriter.WriteAttributeString("count", (vertices.Count * 3).ToString()); - foreach (SceneryVertex vertex in vertices) - { - xmlwriter.WriteValue(vertex.X + XOffset / 4); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(vertex.Y + YOffset / 4); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(vertex.Z + ZOffset / 4); - xmlwriter.WriteWhitespace(" "); - } - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("technique_common"); - xmlwriter.WriteStartElement("accessor"); - xmlwriter.WriteAttributeString("source", "#positions-array"); - xmlwriter.WriteAttributeString("count", vertices.Count.ToString()); - xmlwriter.WriteAttributeString("stride", "3"); - xmlwriter.WriteStartElement("param"); - xmlwriter.WriteAttributeString("name", "X"); - xmlwriter.WriteAttributeString("type", "float"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("param"); - xmlwriter.WriteAttributeString("name", "Y"); - xmlwriter.WriteAttributeString("type", "float"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("param"); - xmlwriter.WriteAttributeString("name", "Y"); - xmlwriter.WriteAttributeString("type", "float"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("source"); - xmlwriter.WriteAttributeString("id", "colors"); - xmlwriter.WriteStartElement("float_array"); - xmlwriter.WriteAttributeString("id", "colors-array"); - xmlwriter.WriteAttributeString("count", (vertices.Count * 3).ToString()); - foreach (SceneryVertex vertex in vertices) - { - if (vertex.Color < Colors.Count) - { - SceneryColor color = Colors[vertex.Color]; - xmlwriter.WriteValue(color.Red / 256.0); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(color.Green / 256.0); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(color.Blue / 256.0); - xmlwriter.WriteWhitespace(" "); - } - else - { - xmlwriter.WriteValue(256.0 / 256.0); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(0 / 256.0); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(256.0 / 256.0); - xmlwriter.WriteWhitespace(" "); - } - } - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("technique_common"); - xmlwriter.WriteStartElement("accessor"); - xmlwriter.WriteAttributeString("source", "#colors-array"); - xmlwriter.WriteAttributeString("count", vertices.Count.ToString()); - xmlwriter.WriteAttributeString("stride", "3"); - xmlwriter.WriteStartElement("param"); - xmlwriter.WriteAttributeString("name", "R"); - xmlwriter.WriteAttributeString("type", "float"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("param"); - xmlwriter.WriteAttributeString("name", "G"); - xmlwriter.WriteAttributeString("type", "float"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("param"); - xmlwriter.WriteAttributeString("name", "B"); - xmlwriter.WriteAttributeString("type", "float"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("vertices"); - xmlwriter.WriteAttributeString("id", "vertices"); - xmlwriter.WriteStartElement("input"); - xmlwriter.WriteAttributeString("semantic", "POSITION"); - xmlwriter.WriteAttributeString("source", "positions"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("input"); - xmlwriter.WriteAttributeString("semantic", "COLOR"); - xmlwriter.WriteAttributeString("source", "colors"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("triangles"); - xmlwriter.WriteAttributeString("count", triangles.Count.ToString()); - xmlwriter.WriteStartElement("input"); - xmlwriter.WriteAttributeString("semantic", "VERTEX"); - xmlwriter.WriteAttributeString("source", "vertices"); - xmlwriter.WriteAttributeString("offset", "0"); - xmlwriter.WriteEndElement(); - xmlwriter.WriteStartElement("p"); - foreach (SceneryTriangle triangle in triangles) - { - xmlwriter.WriteValue(triangle.VertexA); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(triangle.VertexB); - xmlwriter.WriteWhitespace(" "); - xmlwriter.WriteValue(triangle.VertexC); - xmlwriter.WriteWhitespace(" "); - } - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndElement(); - xmlwriter.WriteEndDocument(); - } - return stream.ToArray(); - } - }*/ } } diff --git a/CrashEdit/Controllers/Animation/AnimationEntryController.cs b/CrashEdit/Controllers/Animation/AnimationEntryController.cs index adef6d86..75166654 100644 --- a/CrashEdit/Controllers/Animation/AnimationEntryController.cs +++ b/CrashEdit/Controllers/Animation/AnimationEntryController.cs @@ -1,3 +1,4 @@ +using System.IO; using Crash; using System.Windows.Forms; @@ -14,8 +15,32 @@ public AnimationEntryController(EntryChunkController entrychunkcontroller, Anima } InvalidateNode(); InvalidateNodeImage(); + AddMenu ("Export as OBJ", Menu_Export_OBJ_Game); } + private void Menu_Export_OBJ_Game () + { + if (!FileUtil.SelectSaveFile (out string output, FileFilters.OBJ, FileFilters.Any)) + return; + + // modify the path to add a number before the extension + string ext = Path.GetExtension (output); + string filename = Path.GetFileNameWithoutExtension (output); + string path = Path.GetDirectoryName (output); + + int id = 0; + int count = Node.Nodes.Count.ToString().Length; + + foreach (TreeNode node in Node.Nodes) + { + if (node.Tag is not FrameController frame) + continue; + + frame.ToOBJ (path, filename + id.ToString().PadLeft (count, '0')); + id++; + } + } + public override void InvalidateNode() { Node.Text = string.Format(Crash.UI.Properties.Resources.AnimationEntryController_Text, AnimationEntry.EName); diff --git a/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs b/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs index f9b85d4d..300090fc 100644 --- a/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs +++ b/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs @@ -1,3 +1,4 @@ +using System.IO; using Crash; using System.Windows.Forms; @@ -13,10 +14,35 @@ public ColoredAnimationEntryController(EntryChunkController entrychunkcontroller { AddNode(new ColoredFrameController(this, frame)); } + AddMenu ("Export as OBJ", Menu_Export_OBJ); + InvalidateNode(); InvalidateNodeImage(); } + private void Menu_Export_OBJ() + { + if (!FileUtil.SelectSaveFile (out string output, FileFilters.OBJ, FileFilters.Any)) + return; + + // modify the path to add a number before the extension + string ext = Path.GetExtension (output); + string filename = Path.GetFileNameWithoutExtension (output); + string path = Path.GetDirectoryName (output); + + int id = 0; + int count = Node.Nodes.Count.ToString().Length; + + foreach (TreeNode node in Node.Nodes) + { + if (node.Tag is not ColoredFrameController frame) + continue; + + frame.ToOBJ (path, filename + id.ToString().PadLeft (count, '0')); + id++; + } + } + public override void InvalidateNode() { Node.Text = string.Format(Crash.UI.Properties.Resources.ColoredAnimationEntryController_Text, ColoredAnimationEntry.EName); diff --git a/CrashEdit/Controllers/Animation/ColoredFrameController.cs b/CrashEdit/Controllers/Animation/ColoredFrameController.cs index efeaea89..a739c363 100644 --- a/CrashEdit/Controllers/Animation/ColoredFrameController.cs +++ b/CrashEdit/Controllers/Animation/ColoredFrameController.cs @@ -1,5 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; using Crash; using System.Windows.Forms; +using CrashEdit.Exporters; +using OpenTK; namespace CrashEdit { @@ -9,7 +17,7 @@ public ColoredFrameController(ColoredAnimationEntryController coloranimationentr { ColorAnimationEntryController = coloranimationentrycontroller; OldFrame = oldframe; - AddMenu("Export as OBJ", Menu_Export_OBJ); + AddMenu ("Export as OBJ", Menu_Export_OBJ); InvalidateNode(); InvalidateNodeImage(); } @@ -35,16 +43,108 @@ protected override Control CreateEditor() private void Menu_Export_OBJ() { - OldModelEntry modelentry = ColorAnimationEntryController.EntryChunkController.NSFController.NSF.GetEntry(OldFrame.ModelEID); - if (modelentry == null) + if (!FileUtil.SelectSaveFile (out string filename, FileFilters.OBJ, FileFilters.Any)) + return; + + ToOBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); + } + + public void ToOBJ(string path, string modelname) + { + var exporter = new OBJExporter (); + var model = this.ColorAnimationEntryController.NSF.GetEntry(OldFrame.ModelEID); + var offset = new Vector3 (OldFrame.XOffset, OldFrame.YOffset, OldFrame.ZOffset); + var scale = new Vector3 (model.ScaleX, model.ScaleY, model.ScaleZ) / (GameScales.ModelC1 * GameScales.AnimC1); + + // detect how many textures are used an ther eids to prepare the image + Dictionary textureEIDs = new (); + + foreach (OldModelStruct str in model.Structs) { - throw new GUIException("The linked model entry could not be found."); + if (str is not OldModelTexture tex) + continue; + + if (textureEIDs.ContainsKey (tex.EID)) + continue; + + textureEIDs [tex.EID] = textureEIDs.Count; } - if (MessageBox.Show("Texture and color information will not be exported.\n\nContinue anyway?", "Export as OBJ", MessageBoxButtons.YesNo) != DialogResult.Yes) + + Dictionary objTranslate = new Dictionary (); + + foreach (OldModelPolygon polygon in model.Polygons) { - return; + string material = null; + Vector2? uv1 = null, uv2 = null, uv3 = null; + OldModelStruct str = model.Structs[polygon.Unknown & 0x7FFF]; + OldFrameVertex ov1 = OldFrame.Vertices [polygon.VertexA / 6]; + OldFrameVertex ov2 = OldFrame.Vertices [polygon.VertexB / 6]; + OldFrameVertex ov3 = OldFrame.Vertices [polygon.VertexC / 6]; + + Vector3 v1 = new Vector3 (ov1.X, ov1.Y, ov1.Z); + Vector3 v2 = new Vector3 (ov2.X, ov2.Y, ov2.Z); + Vector3 v3 = new Vector3 (ov3.X, ov3.Y, ov3.Z); + Vector3 color = Vector3.Zero; + + if (str is OldModelTexture t) + { + color = new Vector3 (t.R, t.G, t.B) / 255F; + + // add the texture to the list too + material = objTranslate.FirstOrDefault (x => + x.Value.color == t.ColorMode && + x.Value.blend == t.BlendMode && + x.Value.clutx == t.ClutX && + x.Value.cluty == t.ClutY && + x.Value.page == textureEIDs [t.EID] + ).Key; + + if (material is null) + { + var texinfo = new TexInfoUnpacked( + true, color: t.ColorMode, blend: t.BlendMode, + clutx: t.ClutX, cluty: t.ClutY, + face: Convert.ToInt32(t.N), + page: textureEIDs[t.EID] + ); + + var tpag = this.ColorAnimationEntryController.NSF.GetEntry (t.EID); + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture (((int) texinfo).ToString ("X8"), texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = t.ColorMode switch + { + 0 => new Vector2 (1024, 128), + 1 => new Vector2 (512, 128), + _ => new Vector2 (256, 128) + }; + + uv3 = new Vector2 (t.U3 / texsize.X, (128 - t.V3) / texsize.Y); + uv2 = new Vector2 (t.U2 / texsize.X, (128 - t.V2) / texsize.Y); + uv1 = new Vector2 (t.U1 / texsize.X, (128 - t.V1) / texsize.Y); + } + else if(str is OldSceneryColor c) + { + color = new Vector3 (c.R, c.G, c.B) / 255F; + } + + exporter.AddFace ( + (v1 + offset) * scale, + (v2 + offset) * scale, + (v3 + offset) * scale, + color, color, color, + material, + uv1, uv2, uv3 + ); } - FileUtil.SaveFile(OldFrame.ToOBJ(modelentry), FileFilters.OBJ, FileFilters.Any); + + exporter.Export (path, modelname); } } } diff --git a/CrashEdit/Controllers/Animation/FrameController.cs b/CrashEdit/Controllers/Animation/FrameController.cs index 1050fd9b..8fbcd099 100644 --- a/CrashEdit/Controllers/Animation/FrameController.cs +++ b/CrashEdit/Controllers/Animation/FrameController.cs @@ -1,5 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Linq; using Crash; using System.Windows.Forms; +using CrashEdit.Exporters; +using OpenTK; namespace CrashEdit { @@ -11,8 +20,37 @@ public FrameController(AnimationEntryController animationentrycontroller, Frame Frame = frame; InvalidateNode(); InvalidateNodeImage(); + AddMenu ("Export as OBJ", Menu_Export_OBJ); } + private void Menu_Export_OBJ () + { + if (!FileUtil.SelectSaveFile (out string filename, FileFilters.OBJ, FileFilters.Any)) + return; + + ToOBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); + } + + /// + /// Exports the model to the OBJ file format ready to be used with other software + /// + /// TODO: MAYBE IMPLEMENT AN FBX EXPORT OR SOMETHING ELSE THAT IS A BIT MORE FLEXIBLE? + /// + /// This function resides here because access to GameScales is required, and the Frame object does not have access to it + /// a good improvement might be to move this there + /// + /// + public void ToOBJ (string path, string modelname) + { + Dictionary textureEIDs = new (); + Dictionary objTranslate = new Dictionary (); + + var exporter = new OBJExporter (); + + exporter.AddFrame (AnimationEntryController.NSF, Frame, ref textureEIDs, ref objTranslate); + exporter.Export (path, modelname); + } + public override void InvalidateNode() { Node.Text = Crash.UI.Properties.Resources.FrameController_Text; diff --git a/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs b/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs index 781f7bf9..f72f2b10 100755 --- a/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs +++ b/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs @@ -1,3 +1,4 @@ +using System.IO; using Crash; using System.Windows.Forms; @@ -14,8 +15,32 @@ public OldAnimationEntryController(EntryChunkController entrychunkcontroller, Ol } InvalidateNode(); InvalidateNodeImage(); + AddMenu ("Export as OBJ", Menu_Export_OBJ); } + private void Menu_Export_OBJ () + { + if (!FileUtil.SelectSaveFile (out string output, FileFilters.OBJ, FileFilters.Any)) + return; + + // modify the path to add a number before the extension + string ext = Path.GetExtension (output); + string filename = Path.GetFileNameWithoutExtension (output); + string path = Path.GetDirectoryName (output); + + int id = 0; + int count = Node.Nodes.Count.ToString().Length; + + foreach (TreeNode node in Node.Nodes) + { + if (node.Tag is not OldFrameController frame) + continue; + + frame.ToOBJ (path, filename + id.ToString().PadLeft (count, '0')); + id++; + } + } + public override void InvalidateNode() { Node.Text = string.Format(Crash.UI.Properties.Resources.OldAnimationEntryController_Text, OldAnimationEntry.EName); diff --git a/CrashEdit/Controllers/Animation/OldFrameController.cs b/CrashEdit/Controllers/Animation/OldFrameController.cs old mode 100755 new mode 100644 index c102c0c7..c4bdf326 --- a/CrashEdit/Controllers/Animation/OldFrameController.cs +++ b/CrashEdit/Controllers/Animation/OldFrameController.cs @@ -1,5 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; using Crash; using System.Windows.Forms; +using CrashEdit.Exporters; +using OpenTK; +using OpenTK.Graphics.OpenGL; namespace CrashEdit { @@ -9,7 +18,7 @@ public OldFrameController(OldAnimationEntryController oldanimationentrycontrolle { OldAnimationEntryController = oldanimationentrycontroller; OldFrame = oldframe; - AddMenu("Export as OBJ", Menu_Export_OBJ); + AddMenu ("Export as OBJ (game geometry)", Menu_Export_OBJ); InvalidateNode(); InvalidateNodeImage(); } @@ -56,16 +65,21 @@ protected override Control CreateEditor() private void Menu_Export_OBJ() { - OldModelEntry modelentry = OldAnimationEntryController.EntryChunkController.NSFController.NSF.GetEntry(OldFrame.ModelEID); - if (modelentry == null) - { - throw new GUIException("The linked model entry could not be found."); - } - if (MessageBox.Show("Texture and color information will not be exported.\n\nContinue anyway?", "Export as OBJ", MessageBoxButtons.YesNo) != DialogResult.Yes) - { + if (!FileUtil.SelectSaveFile (out string filename, FileFilters.OBJ, FileFilters.Any)) return; - } - FileUtil.SaveFile(OldFrame.ToOBJ(modelentry), FileFilters.OBJ, FileFilters.Any); + + ToOBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); + } + + public void ToOBJ(string path, string modelname) + { + Dictionary textureEIDs = new Dictionary (); + Dictionary objTranslate = new Dictionary (); + + var exporter = new OBJExporter (); + + exporter.AddFrame (OldAnimationEntryController.NSF, OldFrame, ref textureEIDs, ref objTranslate); + exporter.Export (path, modelname); } } } diff --git a/CrashEdit/Controllers/NSFController.cs b/CrashEdit/Controllers/NSFController.cs index 6762236c..14f1036b 100755 --- a/CrashEdit/Controllers/NSFController.cs +++ b/CrashEdit/Controllers/NSFController.cs @@ -1,7 +1,13 @@ using Crash; using System; using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; using System.Windows.Forms; +using CrashEdit.Exporters; +using DiscUtils; +using OpenTK; namespace CrashEdit { @@ -34,6 +40,7 @@ public NSFController(NSF nsf, GameVersion gameversion) AddMenuSeparator(); AddMenu(Crash.UI.Properties.Resources.NSFController_AcShowLevel, Menu_ShowLevelC1); AddMenu(Crash.UI.Properties.Resources.NSFController_AcShowLevelZones, Menu_ShowLevelZonesC1); + AddMenu (Crash.UI.Properties.Resources.NSFController_AcExportScenery, Menu_ExportSceneryC1OBJ); } else if (GameVersion == GameVersion.Crash1Beta1995) { @@ -46,6 +53,8 @@ public NSFController(NSF nsf, GameVersion gameversion) AddMenuSeparator(); AddMenu(Crash.UI.Properties.Resources.NSFController_AcShowLevel, Menu_ShowLevelC2); AddMenu(Crash.UI.Properties.Resources.NSFController_AcShowLevelZones, Menu_ShowLevelZonesC2); + AddMenuSeparator (); + AddMenu (Crash.UI.Properties.Resources.NSFController_AcExportScenery, Menu_ExportSceneryC2OBJ); } InvalidateNode(); InvalidateNodeImage(); @@ -361,6 +370,56 @@ private void Menu_ShowLevelZonesC2() }; } + private void Menu_ExportSceneryC1OBJ () + { + if (!FileUtil.SelectSaveFile (out string filename, FileFilters.OBJ, FileFilters.Any)) + return; + + ExportSceneryC1OBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); + } + + private void Menu_ExportSceneryC2OBJ () + { + if (!FileUtil.SelectSaveFile (out string filename, FileFilters.OBJ, FileFilters.Any)) + return; + + ExportSceneryC2OBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); + } + + private void ExportSceneryC2OBJ (string path, string modelname) + { + var exporter = new OBJExporter (); + + // detect how many textures are used and their eids to prepare the image + Dictionary textureEIDs = new (); + Dictionary objTranslate = new Dictionary (); + + // find all the scenery insde chunks + foreach (SceneryEntry scenery in NSF.GetEntries ()) + { + exporter.AddScenery (NSF, scenery, ref textureEIDs, ref objTranslate); + } + + exporter.Export (path, modelname); + } + + private void ExportSceneryC1OBJ (string path, string modelname) + { + var exporter = new OBJExporter (); + + // detect how many textures are used and their eids to prepare the image + Dictionary textureEIDs = new (); + Dictionary objTranslate = new Dictionary (); + + // find all the scenery insde chunks + foreach (OldSceneryEntry scenery in NSF.GetEntries ()) + { + exporter.AddScenery (NSF, scenery, ref textureEIDs, ref objTranslate); + } + + exporter.Export (path, modelname); + } + private void Menu_Import_Chunk() { byte[][] datas = FileUtil.OpenFiles(FileFilters.Any); diff --git a/CrashEdit/Controllers/Scenery/OldSceneryEntryController.cs b/CrashEdit/Controllers/Scenery/OldSceneryEntryController.cs index 178aefbb..3cbbf876 100755 --- a/CrashEdit/Controllers/Scenery/OldSceneryEntryController.cs +++ b/CrashEdit/Controllers/Scenery/OldSceneryEntryController.cs @@ -14,7 +14,6 @@ public OldSceneryEntryController(EntryChunkController entrychunkcontroller, OldS } AddMenuSeparator(); AddMenu("Export as OBJ", Menu_Export_OBJ); - AddMenu("Export as COLLADA", Menu_Export_COLLADA); InvalidateNode(); InvalidateNodeImage(); } @@ -45,14 +44,5 @@ private void Menu_Export_OBJ() } FileUtil.SaveFile(OldSceneryEntry.ToOBJ(), FileFilters.OBJ, FileFilters.Any); } - - private void Menu_Export_COLLADA() - { - if (MessageBox.Show("Exporting to COLLADA is experimental.\nTexture information will not be exported.\n\nContinue anyway?", "Export as COLLADA", MessageBoxButtons.YesNo) != DialogResult.Yes) - { - return; - } - FileUtil.SaveFile(OldSceneryEntry.ToCOLLADA(), FileFilters.COLLADA, FileFilters.Any); - } } } diff --git a/CrashEdit/Controllers/Scenery/SceneryEntryController.cs b/CrashEdit/Controllers/Scenery/SceneryEntryController.cs index 23f8d4c5..64a6e3b8 100755 --- a/CrashEdit/Controllers/Scenery/SceneryEntryController.cs +++ b/CrashEdit/Controllers/Scenery/SceneryEntryController.cs @@ -1,5 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; using Crash; using System.Windows.Forms; +using CrashEdit.Exporters; +using OpenTK; namespace CrashEdit { @@ -10,7 +17,6 @@ public SceneryEntryController(EntryChunkController entrychunkcontroller, Scenery SceneryEntry = sceneryentry; AddMenuSeparator(); AddMenu("Export as Wavefront OBJ", Menu_Export_OBJ); - AddMenu("Export as Stanford PLY", Menu_Export_PLY); //AddMenu("Export as COLLADA",Menu_Export_COLLADA); AddMenu("Fix coords imported from Crash 3", Menu_Fix_WGEOv3); InvalidateNode(); @@ -35,33 +41,193 @@ protected override Control CreateEditor() public SceneryEntry SceneryEntry { get; } - private void Menu_Export_OBJ() + private void Menu_Export_OBJ () { - if (MessageBox.Show("Exporting to Wavefront OBJ (.obj) is experimental.\nTexture and color information will not be exported.\n\nContinue anyway?", "Export as OBJ", MessageBoxButtons.YesNo) != DialogResult.Yes) - { + if (!FileUtil.SelectSaveFile (out string filename, FileFilters.OBJ, FileFilters.Any)) return; - } - FileUtil.SaveFile(SceneryEntry.ToOBJ(), FileFilters.OBJ, FileFilters.Any); + + ToOBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); } - private void Menu_Export_PLY() + private void ToOBJ (string path, string modelname) { - if (MessageBox.Show("Exporting to Stanford PLY (.ply) is experimental.\nTexture information will not be exported.\n\nContinue anyway?", "Export as PLY", MessageBoxButtons.YesNo) != DialogResult.Yes) + var exporter = new OBJExporter (); + var offset = new Vector3 (SceneryEntry.XOffset, SceneryEntry.YOffset, SceneryEntry.ZOffset); + var scale = new Vector3 (1 / GameScales.WorldC1); + + // detect how many textures are used and their eids to prepare the image + Dictionary textureEIDs = new (); + + for (int i = 0; i < SceneryEntry.TPAGCount; i++) { - return; + int tpag_eid = SceneryEntry.GetTPAG (i); + + if (textureEIDs.ContainsKey (tpag_eid)) + continue; + + textureEIDs.Add (tpag_eid, textureEIDs.Count); } - FileUtil.SaveFile(SceneryEntry.ToPLY(), FileFilters.PLY, FileFilters.Any); - } + + Dictionary objTranslate = new Dictionary (); - /*private void Menu_Export_COLLADA() - { - if (MessageBox.Show("Exporting to COLLADA (.dae) is experimental.\nTexture and quad information will not be exported.\n\nContinue anyway?", "Export as OBJ", MessageBoxButtons.YesNo) != DialogResult.Yes) + foreach (var tri in SceneryEntry.Triangles) { - return; + var info = TextureUtils.ProcessTextureInfoC2(0, tri.Texture, tri.Animated, SceneryEntry.Textures, SceneryEntry.AnimatedTextures); + string material = null; + Vector2? uv1 = null, uv2 = null, uv3 = null; + + if (info.Item1 && info.Item2 is not null) + { + var value = info.Item2.Value; + + material = objTranslate.FirstOrDefault (x => + x.Value.color == value.ColorMode && + x.Value.blend == value.BlendMode && + x.Value.clutx == value.ClutX && + x.Value.cluty == value.ClutY && + x.Value.page == textureEIDs [SceneryEntry.GetTPAG (value.Page)] + ).Key; + + // ignore the texinfo if there's already a texture with the exact same settings stored + if (material is null) + { + int textureEID = SceneryEntry.GetTPAG (value.Page); + + var texinfo = new TexInfoUnpacked ( + true, color: value.ColorMode, blend: value.BlendMode, clutx: value.ClutX, cluty: value.ClutY, + page: textureEIDs [textureEID] + ); + + var tpag = this.NSF.GetEntry (textureEIDs.First (x => x.Key == textureEID).Key); + + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture (((int) texinfo).ToString ("X8"), texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = value.ColorMode switch + { + 0 => new Vector2 (1024, 128), + 1 => new Vector2 (512, 128), + _ => new Vector2 (256, 128) + }; + + uv1 = new (value.X2 / texsize.X, (128 - value.Y2) / texsize.Y); + uv2 = new Vector2 (value.X1 / texsize.X, (128 - value.Y1) / texsize.Y); + uv3 = new (value.X3 / texsize.X, (128 - value.Y3) / texsize.Y); + } + + // add the face + SceneryVertex fv1 = SceneryEntry.Vertices[tri.VertexA]; + SceneryVertex fv2 = SceneryEntry.Vertices[tri.VertexB]; + SceneryVertex fv3 = SceneryEntry.Vertices[tri.VertexC]; + SceneryColor fc1 = SceneryEntry.Colors [fv1.Color]; + SceneryColor fc2 = SceneryEntry.Colors [fv2.Color]; + SceneryColor fc3 = SceneryEntry.Colors [fv3.Color]; + Vector3 v1 = new Vector3 (fv1.X, fv1.Y, fv1.Z); + Vector3 v2 = new Vector3 (fv2.X, fv2.Y, fv2.Z); + Vector3 v3 = new Vector3 (fv3.X, fv3.Y, fv3.Z); + Vector3 c1 = new Vector3 (fc1.Red, fc1.Green, fc1.Blue) / 255f; + Vector3 c2 = new Vector3 (fc2.Red, fc2.Green, fc2.Blue) / 255f; + Vector3 c3 = new Vector3 (fc3.Red, fc3.Green, fc3.Blue) / 255f; + + exporter.AddFace ( + (v1 + offset / 16) * scale, + (v2 + offset / 16) * scale, + (v3 + offset / 16) * scale, + c1, c2, c3, + material, + uv1, uv2, uv3 + ); } - FileUtil.SaveFile(sceneryentry.ToCOLLADA(), FileFilters.COLLADA, FileFilters.Any); - }*/ + + foreach (var quad in SceneryEntry.Quads) + { + var info = TextureUtils.ProcessTextureInfoC2(0, quad.Texture, quad.Animated, SceneryEntry.Textures, SceneryEntry.AnimatedTextures); + string material = null; + Vector2? uv1 = null, uv2 = null, uv3 = null, uv4 = null; + + if (info.Item1 && info.Item2 is not null) + { + var value = info.Item2.Value; + int textureEID = SceneryEntry.GetTPAG (value.Page); + + material = objTranslate.FirstOrDefault (x => + x.Value.color == value.ColorMode && + x.Value.blend == value.BlendMode && + x.Value.clutx == value.ClutX && + x.Value.cluty == value.ClutY && + x.Value.page == textureEIDs [textureEID] + ).Key; + + // ignore the texinfo if there's already a texture with the exact same settings stored + if (material is null) + { + var texinfo = new TexInfoUnpacked ( + true, color: value.ColorMode, blend: value.BlendMode, clutx: value.ClutX, cluty: value.ClutY, + page: textureEIDs [textureEID] + ); + var tpag = this.NSF.GetEntry (textureEIDs.First (x => x.Key == textureEID).Key); + + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture (((int) texinfo).ToString ("X8"), texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = value.ColorMode switch + { + 0 => new Vector2 (1024, 128), + 1 => new Vector2 (512, 128), + _ => new Vector2 (256, 128) + }; + + uv1 = new (value.X2 / texsize.X, (128 - value.Y2) / texsize.Y); + uv2 = new Vector2 (value.X1 / texsize.X, (128 - value.Y1) / texsize.Y); + uv3 = new (value.X3 / texsize.X, (128 - value.Y3) / texsize.Y); + uv4 = new (value.X4 / texsize.X, (128 - value.Y4) / texsize.Y); + } + + // add the face + SceneryVertex fv1 = SceneryEntry.Vertices[quad.VertexA]; + SceneryVertex fv2 = SceneryEntry.Vertices[quad.VertexB]; + SceneryVertex fv3 = SceneryEntry.Vertices[quad.VertexC]; + SceneryVertex fv4 = SceneryEntry.Vertices[quad.VertexD]; + SceneryColor fc1 = SceneryEntry.Colors [fv1.Color]; + SceneryColor fc2 = SceneryEntry.Colors [fv2.Color]; + SceneryColor fc3 = SceneryEntry.Colors [fv3.Color]; + SceneryColor fc4 = SceneryEntry.Colors [fv4.Color]; + Vector3 v1 = new Vector3 (fv1.X, fv1.Y, fv1.Z); + Vector3 v2 = new Vector3 (fv2.X, fv2.Y, fv2.Z); + Vector3 v3 = new Vector3 (fv3.X, fv3.Y, fv3.Z); + Vector3 v4 = new Vector3 (fv4.X, fv4.Y, fv4.Z); + Vector3 c1 = new Vector3 (fc1.Red, fc1.Green, fc1.Blue) / 255f; + Vector3 c2 = new Vector3 (fc2.Red, fc2.Green, fc2.Blue) / 255f; + Vector3 c3 = new Vector3 (fc3.Red, fc3.Green, fc3.Blue) / 255f; + Vector3 c4 = new Vector3 (fc4.Red, fc4.Green, fc4.Blue) / 255f; + + exporter.AddFace ( + (v1 + offset / 16) * scale, + (v2 + offset / 16) * scale, + (v3 + offset / 16) * scale, + (v4 + offset / 16) * scale, + c1, c2, c3, c4, + material, + uv1, uv2, uv3, uv4 + ); + } + + exporter.Export (path, modelname); + } + private void Menu_Fix_WGEOv3() { for (int i = 0; i < SceneryEntry.Vertices.Count; i++) diff --git a/CrashEdit/Controls/3D/AnimationEntryViewer.cs b/CrashEdit/Controls/3D/AnimationEntryViewer.cs index 89047ffe..50c6ccd3 100644 --- a/CrashEdit/Controls/3D/AnimationEntryViewer.cs +++ b/CrashEdit/Controls/3D/AnimationEntryViewer.cs @@ -225,7 +225,7 @@ private void RenderFrame(Frame frame, int buf) } foreach (var tri in model.Triangles) { - var polygon_texture_info = ProcessTextureInfoC2(tri.Texture, tri.Animated, model.Textures, model.AnimatedTextures); + var polygon_texture_info = TextureUtils.ProcessTextureInfoC2 (this.render.RealCurrentFrame, tri.Texture, tri.Animated, model.Textures, model.AnimatedTextures); if (!polygon_texture_info.Item1) continue; bool nocull = tri.Subtype == 0 || tri.Subtype == 2; diff --git a/CrashEdit/Controls/3D/GLViewer.cs b/CrashEdit/Controls/3D/GLViewer.cs index 38b21a0c..314bd67d 100644 --- a/CrashEdit/Controls/3D/GLViewer.cs +++ b/CrashEdit/Controls/3D/GLViewer.cs @@ -973,56 +973,6 @@ protected void SetupTPAGs(Dictionary tex_eids) } } - protected Tuple ProcessTextureInfoC2(int in_tex_id, bool animated, IList textures, IList animated_textures) - { - if (in_tex_id != 0 || animated) - { - ModelTexture? info_temp = null; - int tex_id = in_tex_id - 1; - if (animated) - { - if (++tex_id >= animated_textures.Count) - { - return new(false, null); - } - var anim = animated_textures[tex_id]; - // check if it's an untextured polygon - if (anim.Offset != 0) - { - tex_id = anim.Offset - 1; - if (anim.IsLOD) - { - tex_id += anim.LOD0; // we only render closest LOD for now - } - else - { - tex_id += (int)((render.RealCurrentFrame / 2 / (1 + anim.Latency) + anim.Delay) & anim.Mask); - if (anim.Leap) - { - anim = animated_textures[++tex_id]; - tex_id = anim.Offset - 1 + anim.LOD0; - } - } - if (tex_id >= textures.Count) - { - return new(false, null); - } - info_temp = textures[tex_id]; - } - } - else - { - if (tex_id >= textures.Count) - { - return new(false, null); - } - info_temp = textures[tex_id]; - } - return new(true, info_temp); - } - return new(true, null); - } - protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); diff --git a/CrashEdit/Controls/3D/SceneryEntryViewer.cs b/CrashEdit/Controls/3D/SceneryEntryViewer.cs index 6d0b4b7b..11652092 100644 --- a/CrashEdit/Controls/3D/SceneryEntryViewer.cs +++ b/CrashEdit/Controls/3D/SceneryEntryViewer.cs @@ -133,7 +133,7 @@ protected void RenderWorld(SceneryEntry world, Dictionary tex_eids) foreach (SceneryTriangle tri in world.Triangles) { if (tri.VertexA >= world.Vertices.Count || tri.VertexB >= world.Vertices.Count || tri.VertexC >= world.Vertices.Count) continue; - var polygon_texture_info = ProcessTextureInfoC2(tri.Texture, tri.Animated, world.Textures, world.AnimatedTextures); + var polygon_texture_info = TextureUtils.ProcessTextureInfoC2(render.RealCurrentFrame, tri.Texture, tri.Animated, world.Textures, world.AnimatedTextures); if (!polygon_texture_info.Item1) continue; VertexTexInfo tex = new(false); // completely untextured @@ -156,7 +156,7 @@ protected void RenderWorld(SceneryEntry world, Dictionary tex_eids) foreach (SceneryQuad quad in world.Quads) { if (quad.VertexA >= world.Vertices.Count || quad.VertexB >= world.Vertices.Count || quad.VertexC >= world.Vertices.Count || quad.VertexD >= world.Vertices.Count) continue; - var polygon_texture_info = ProcessTextureInfoC2(quad.Texture, quad.Animated, world.Textures, world.AnimatedTextures); + var polygon_texture_info = TextureUtils.ProcessTextureInfoC2(render.RealCurrentFrame, quad.Texture, quad.Animated, world.Textures, world.AnimatedTextures); if (!polygon_texture_info.Item1) continue; VertexTexInfo tex = new(false); // completely untextured diff --git a/CrashEdit/CrashEdit.csproj b/CrashEdit/CrashEdit.csproj index 358b56b9..f414c2eb 100755 --- a/CrashEdit/CrashEdit.csproj +++ b/CrashEdit/CrashEdit.csproj @@ -160,6 +160,11 @@ UserControl + + + + + Form @@ -389,6 +394,7 @@ + @@ -600,7 +606,6 @@ false - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. diff --git a/CrashEdit/Exporters/FrameExtensions.cs b/CrashEdit/Exporters/FrameExtensions.cs new file mode 100644 index 00000000..e379b22e --- /dev/null +++ b/CrashEdit/Exporters/FrameExtensions.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using Crash; +using OpenTK; + +namespace CrashEdit.Exporters; + +// TODO: ALL FRAMES CLASSES/STRUCTS SHOULD HAVE A BASE INTERFACE WITH DATA IN COMMON +// TODO: THAT WOULD MAKE WORKING WITH THEM EASIER FOR THINGS LIKE THESE WHERE YOU ONLY NEED THE DATA +// TODO: THEY HAVE IN COMMON, BUT CHANGING THAT IS OUT OF THE SCOPE OF THESE COMMITS +// TODO: BUT THAT WOULD CUT DOWN THE METHODS HERE TO JUST ONE OR TWO +public static class FrameExtensions +{ + public static void AddFrame (this OBJExporter exporter, NSF nsf, OldFrame frame, ref Dictionary textureEIDs, ref Dictionary objTranslate) + { + var model = nsf.GetEntry(frame.ModelEID); + var offset = new Vector3 (frame.XOffset, frame.YOffset, frame.ZOffset); + var scale = new Vector3 (model.ScaleX, model.ScaleY, model.ScaleZ) / (GameScales.ModelC1 * GameScales.AnimC1); + + foreach (OldModelStruct str in model.Structs) + { + if (str is not OldModelTexture tex) + continue; + + if (textureEIDs.ContainsKey (tex.EID)) + continue; + + textureEIDs [tex.EID] = textureEIDs.Count; + } + + foreach (OldModelPolygon polygon in model.Polygons) + { + string material = null; + Vector2? uv1 = null, uv2 = null, uv3 = null; + OldModelStruct str = model.Structs [polygon.Unknown & 0x7FFF]; + OldFrameVertex ov1 = frame.Vertices [polygon.VertexA / 6]; + OldFrameVertex ov2 = frame.Vertices [polygon.VertexB / 6]; + OldFrameVertex ov3 = frame.Vertices [polygon.VertexC / 6]; + Vector3 v1 = new Vector3 (ov1.X, ov1.Y, ov1.Z); + Vector3 v2 = new Vector3 (ov2.X, ov2.Y, ov2.Z); + Vector3 v3 = new Vector3 (ov3.X, ov3.Y, ov3.Z); + Vector3 color = Vector3.Zero; + + if (str is OldModelTexture t) + { + material = exporter.AddTexture (nsf, t, ref textureEIDs, ref objTranslate, out color, out uv1, out uv2, out uv3); + } + else if (str is OldSceneryColor c) + { + color = new Vector3 (c.R, c.G, c.B) / 255F; + } + + exporter.AddFace ( + (v1 + offset) * scale, + (v2 + offset) * scale, + (v3 + offset) * scale, + color, color, color, + material, + uv1, uv2, uv3 + ); + } + } + + public static void AddFrame (this OBJExporter exporter, NSF nsf, Frame frame, ref Dictionary textureEIDs, ref Dictionary objTranslate) + { + // TODO: SUPPORT CRASH2 AND CRASH3 PROPER SCALING + // offset correction is 4f in Crash2, 32f in Crash3 + var model = nsf.GetEntry(frame.ModelEID); + var vertices = frame.MakeVertices (model); + var offset = new Vector3 (frame.XOffset, frame.YOffset, frame.ZOffset) / 4F; + var scale = new Vector3 (model.ScaleX, model.ScaleY, model.ScaleZ) / GameScales.ModelC1 / GameScales.AnimC1; + + // detect how many textures are used and their eids to prepare the image + + for (int i = 0; i < model.TPAGCount; i++) + { + int tpag_eid = model.GetTPAG (i); + + if (textureEIDs.ContainsKey (tpag_eid)) + continue; + + textureEIDs.Add (tpag_eid, textureEIDs.Count); + } + + // once we have the textureEIDs we know what has to be loaded where + // these textures are 128 pixels of height + // and the game loads every texture used and keeps a lookup table of the texture "index" + + // depending on where in the model it's going to be drawn, the texture is treated differently + // (4 bits per pixel, 8 bits per pixel, 16 bits per pixel) + // that information is inside each triangle of the frame + // so the same texture can be treated differently + // try to build an atlas of sorts with all the information + + // iterate all the triangles, get the texture modes and build information about those + foreach (var tri in model.Triangles) + { + string material = exporter.AddTexture ( + nsf, tri, model, ref textureEIDs, ref objTranslate, + out var uv1, out var uv2, out var uv3, + out bool flip + ); + + // add the face + SceneryColor fc1 = model.Colors [tri.Color [!flip ? 0 : 2]]; + SceneryColor fc2 = model.Colors [tri.Color [!flip ? 1 : 1]]; + SceneryColor fc3 = model.Colors [tri.Color [!flip ? 2 : 0]]; + Position fv1 = vertices [tri.Vertex [!flip ? 0 : 2] + frame.SpecialVertexCount]; + Position fv2 = vertices [tri.Vertex [!flip ? 1 : 1] + frame.SpecialVertexCount]; + Position fv3 = vertices [tri.Vertex [!flip ? 2 : 0] + frame.SpecialVertexCount]; + Vector3 v1 = new Vector3 (fv1.X, fv1.Z, fv1.Y); + Vector3 v2 = new Vector3 (fv2.X, fv2.Z, fv2.Y); + Vector3 v3 = new Vector3 (fv3.X, fv3.Z, fv3.Y); + Vector3 c1 = new Vector3 (fc1.Red, fc1.Green, fc1.Blue) / 255f; + Vector3 c2 = new Vector3 (fc2.Red, fc2.Green, fc2.Blue) / 255f; + Vector3 c3 = new Vector3 (fc3.Red, fc3.Green, fc3.Blue) / 255f; + + exporter.AddFace ( + (v1 + offset) * scale, + (v2 + offset) * scale, + (v3 + offset) * scale, + c1, c2, c3, + material, + uv1, uv2, uv3 + ); + } + } +} \ No newline at end of file diff --git a/CrashEdit/Exporters/MaterialExtensions.cs b/CrashEdit/Exporters/MaterialExtensions.cs new file mode 100644 index 00000000..d8d76283 --- /dev/null +++ b/CrashEdit/Exporters/MaterialExtensions.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using Crash; +using OpenTK; + +namespace CrashEdit.Exporters; + +// TODO: ALL VERTEX/QUAD CLASSES/STRUCTS SHOULD HAVE A BASE INTERFACE WITH DATA IN COMMON +// TODO: THAT WOULD MAKE WORKING WITH THEM EASIER FOR THINGS LIKE THESE WHERE YOU ONLY NEED THE DATA +// TODO: THEY HAVE IN COMMON, BUT CHANGING THAT IS OUT OF THE SCOPE OF THESE COMMITS +// TODO: BUT THAT WOULD CUT DOWN THE METHODS HERE TO JUST ONE OR TWO +public static class MaterialExtensions +{ + private static Vector2 TexSize (int colorMode) + { + return colorMode switch + { + 0 => new Vector2 (1024, 128), + 1 => new Vector2 (512, 128), + _ => new Vector2 (256, 128) + }; + } + + public static string AddTexture (this OBJExporter exporter, NSF nsf, ModelTransformedTriangle tri, ModelEntry model, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3, out bool flip) + { + var info = TextureUtils.ProcessTextureInfoC2 (0, tri.Texture, tri.Animated, model.Textures, model.AnimatedTextures); + string material = null; + uv1 = uv2 = uv3 = null; + bool nocull = tri.Subtype == 0 || tri.Subtype == 2; + flip = (tri.Type == 2 ^ tri.Subtype == 3) && !nocull; + + // parse the texture and add it to the exporter + if (info.Item1 && info.Item2 is not null) + { + var value = info.Item2.Value; + int textureEID = model.GetTPAG (value.Page); + int page = textureEIDs [textureEID]; + + material = objTranslate.FirstOrDefault (x => + x.Value.color == value.ColorMode && + x.Value.blend == value.BlendMode && + x.Value.clutx == value.ClutX && + x.Value.cluty == value.ClutY && + x.Value.page == page + ).Key; + + // ignore the texinfo if there's already a texture with the exact same settings stored + if (material is null) + { + var texinfo = new TexInfoUnpacked ( + true, color: value.ColorMode, blend: value.BlendMode, clutx: value.ClutX, cluty: value.ClutY, + page: textureEIDs [textureEID] + ); + + var tpag = nsf.GetEntry (textureEIDs.First (x => x.Key == textureEID).Key); + + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture ($"{textureEID}-{((int)texinfo).ToString("X8")}c{texinfo.color}b{texinfo.blend}", texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = TexSize (value.ColorMode); + + uv2 = new Vector2 (value.X2 / texsize.X, (128 - value.Y2) / texsize.Y); + + if ((tri.Type != 2 && !flip) || (tri.Type == 2 && tri.Subtype == 1)) + { + uv1 = new (value.X3 / texsize.X, (128 - value.Y3) / texsize.Y); + uv3 = new (value.X1 / texsize.X, (128 - value.Y1) / texsize.Y); + } + else + { + uv3 = new (value.X3 / texsize.X, (128 - value.Y3) / texsize.Y); + uv1 = new (value.X1 / texsize.X, (128 - value.Y1) / texsize.Y); + } + } + + return material; + } + + public static string AddTexture (this OBJExporter exporter, NSF nsf, SceneryTriangle tri, SceneryEntry scenery, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3) + { + var info = TextureUtils.ProcessTextureInfoC2 (0, tri.Texture, tri.Animated, scenery.Textures, scenery.AnimatedTextures); + string material = null; + uv1 = uv2 = uv3 = null; + + if (info.Item1 && info.Item2 is not null) + { + var value = info.Item2.Value; + int textureEID = scenery.GetTPAG (value.Page); + int page = textureEIDs [textureEID]; + + material = objTranslate.FirstOrDefault (x => + x.Value.color == value.ColorMode && + x.Value.blend == value.BlendMode && + x.Value.clutx == value.ClutX && + x.Value.cluty == value.ClutY && + x.Value.page == page + ).Key; + + // ignore the texinfo if there's already a texture with the exact same settings stored + if (material is null) + { + var texinfo = new TexInfoUnpacked ( + true, color: value.ColorMode, blend: value.BlendMode, clutx: value.ClutX, cluty: value.ClutY, + page: textureEIDs [textureEID] + ); + + var tpag = nsf.GetEntry (textureEIDs.First (x => x.Key == textureEID).Key); + + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture ($"{textureEID}-{((int)texinfo).ToString("X8")}c{texinfo.color}b{texinfo.blend}", texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = TexSize (value.ColorMode); + + uv1 = new (value.X2 / texsize.X, (128 - value.Y2) / texsize.Y); + uv2 = new Vector2 (value.X1 / texsize.X, (128 - value.Y1) / texsize.Y); + uv3 = new (value.X3 / texsize.X, (128 - value.Y3) / texsize.Y); + } + + return material; + } + + public static string AddTexture (this OBJExporter exporter, NSF nsf, SceneryQuad quad, SceneryEntry scenery, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3, out Vector2? uv4) + { + var info = TextureUtils.ProcessTextureInfoC2 (0, quad.Texture, quad.Animated, scenery.Textures, scenery.AnimatedTextures); + string material = null; + uv1 = uv2 = uv3 = uv4 = null; + + if (info.Item1 && info.Item2 is not null) + { + var value = info.Item2.Value; + int textureEID = scenery.GetTPAG (value.Page); + int page = textureEIDs [textureEID]; + + material = objTranslate.FirstOrDefault (x => + x.Value.color == value.ColorMode && + x.Value.blend == value.BlendMode && + x.Value.clutx == value.ClutX && + x.Value.cluty == value.ClutY && + x.Value.page == page + ).Key; + + // ignore the texinfo if there's already a texture with the exact same settings stored + if (material is null) + { + var texinfo = new TexInfoUnpacked ( + true, color: value.ColorMode, blend: value.BlendMode, clutx: value.ClutX, cluty: value.ClutY, + page: textureEIDs [textureEID] + ); + + var tpag = nsf.GetEntry (textureEIDs.First (x => x.Key == textureEID).Key); + + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture ($"{textureEID}-{((int)texinfo).ToString("X8")}c{texinfo.color}b{texinfo.blend}", texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = TexSize (value.ColorMode); + + uv1 = new (value.X2 / texsize.X, (128 - value.Y2) / texsize.Y); + uv2 = new (value.X1 / texsize.X, (128 - value.Y1) / texsize.Y); + uv3 = new (value.X3 / texsize.X, (128 - value.Y3) / texsize.Y); + uv4 = new (value.X4 / texsize.X, (128 - value.Y4) / texsize.Y); + } + + return material; + } + + public static string AddTexture (this OBJExporter exporter, NSF nsf, OldSceneryTexture t, int textureEID, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector3 color, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3) + { + string material = null; + int page = textureEIDs [textureEID]; + color = new Vector3 (t.R, t.G, t.B) / 255F; + + // add the texture to the list too + material = objTranslate.FirstOrDefault (x => + x.Value.color == t.ColorMode && + x.Value.blend == t.BlendMode && + x.Value.clutx == t.ClutX && + x.Value.cluty == t.ClutY && + x.Value.page == page + ).Key; + + if (material is null) + { + var texinfo = new TexInfoUnpacked( + true, color: t.ColorMode, blend: t.BlendMode, + clutx: t.ClutX, cluty: t.ClutY, + page: textureEIDs[textureEID] + ); + + var tpag = nsf.GetEntry (textureEID); + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture ($"{textureEID}-{((int)texinfo).ToString("X8")}c{texinfo.color}b{texinfo.blend}", texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = TexSize (t.ColorMode); + + uv3 = new Vector2 (t.U1 / texsize.X, (128 - t.V1) / texsize.Y); + uv2 = new Vector2 (t.U2 / texsize.X, (128 - t.V2) / texsize.Y); + uv1 = new Vector2 (t.U3 / texsize.X, (128 - t.V3) / texsize.Y); + + return material; + } + + public static string AddTexture (this OBJExporter exporter, NSF nsf, OldModelTexture t, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector3 color, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3) + { + string material = null; + int page = textureEIDs [t.EID]; + color = new Vector3 (t.R, t.G, t.B) / 255F; + + // add the texture to the list too + material = objTranslate.FirstOrDefault (x => + x.Value.color == t.ColorMode && + x.Value.blend == t.BlendMode && + x.Value.clutx == t.ClutX && + x.Value.cluty == t.ClutY && + x.Value.page == page + ).Key; + + if (material is null) + { + var texinfo = new TexInfoUnpacked ( + true, color: t.ColorMode, blend: t.BlendMode, + clutx: t.ClutX, cluty: t.ClutY, + face: Convert.ToInt32 (t.N), + page: textureEIDs [t.EID] + ); + + var tpag = nsf.GetEntry (t.EID); + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, texinfo); + + // the material name changes + material = exporter.AddTexture ($"{t.EID}-{((int)texinfo).ToString("X8")}c{texinfo.color}b{texinfo.blend}", texture); + + // add it to the lookup table too + objTranslate [material] = texinfo; + } + + Vector2 texsize = TexSize (t.ColorMode); + + uv3 = new Vector2 (t.U3 / texsize.X, (128 - t.V3) / texsize.Y); + uv2 = new Vector2 (t.U2 / texsize.X, (128 - t.V2) / texsize.Y); + uv1 = new Vector2 (t.U1 / texsize.X, (128 - t.V1) / texsize.Y); + + return material; + } +} \ No newline at end of file diff --git a/CrashEdit/Exporters/OBJExporter.cs b/CrashEdit/Exporters/OBJExporter.cs new file mode 100644 index 00000000..0408cce1 --- /dev/null +++ b/CrashEdit/Exporters/OBJExporter.cs @@ -0,0 +1,536 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using OpenTK; + +namespace CrashEdit.Exporters; + +public class OBJExporter +{ + private const string DEFAULT_MATERIAL = "default"; + + class Material + { + public Vector3 ambient; + public Vector3 diffuse; + public Vector3 specular; + public float highlight; + public Bitmap texture; + } + + class Face + { + public int V1; + public int V2; + public int V3; + public int? V4; + public string material; + public int? UV1; + public int? UV2; + public int? UV3; + public int? UV4; + } + + class Vertex + { + public Vector3 position; + public Vector3 color; + } + + private Dictionary materials = new Dictionary (); + private List vertices = new List (); + private List faces = new List (); + private List uvs = new List (); + + public OBJExporter () + { + // create a default material for everything that is not textured + this.materials[DEFAULT_MATERIAL] = new Material + { + ambient = Vector3.One, + diffuse = Vector3.One, + highlight = 0.0f, + specular = Vector3.Zero, + texture = null + }; + } + + /// + /// Adds a texture with the given name to the obj + /// + /// + /// Texture data + /// The identifier for the texture in the obj export + public string AddTexture (string name, Bitmap texture) + { + string identifier = $"tex{name}"; + + this.materials [identifier] = new Material + { + ambient = Vector3.One, + diffuse = Vector3.One, + highlight = 0.0f, + specular = Vector3.Zero, + texture = texture + }; + + return identifier; + } + + /// + /// Adds a new vertex to the output + /// + /// + /// + public void AddVertex (Vector3 position, Vector3 color) + { + this.vertices.Add ( + new Vertex + { + position = position, + color = color + } + ); + } + + /// + /// Adds a simple face using the given vertices + /// + /// + /// + /// + /// + /// + /// + /// + public void AddFace (int v1, int v2, int v3, string material = null, Vector2? uv1 = null, Vector2? uv2 = null, Vector2? uv3 = null) + { + // add uv coordinates to the lists first + int? uv1id = null; + int? uv2id = null; + int? uv3id = null; + + if (uv1 != uv2 || uv1 != uv3) + throw new InvalidDataException ("UVs must all be null or all have values"); + + if (uv1 is not null) + { + uv1id = this.uvs.Count; + uv2id = this.uvs.Count + 1; + uv3id = this.uvs.Count + 2; + + this.uvs.Add (uv1.Value); + this.uvs.Add (uv2.Value); + this.uvs.Add (uv3.Value); + } + + this.faces.Add ( + new Face + { + material = material ?? DEFAULT_MATERIAL, + V1 = v1, + V2 = v2, + V3 = v3, + UV1 = uv1id, + UV2 = uv2id, + UV3 = uv3id + } + ); + } + + /// + /// Adds a simple face using the given vertices + /// + /// + /// + /// + /// + /// + /// + /// + public void AddFace (int v1, int v2, int v3, int v4, string material = null, Vector2? uv1 = null, Vector2? uv2 = null, Vector2? uv3 = null, Vector2? uv4 = null) + { + // add uv coordinates to the lists first + int? uv1id = null; + int? uv2id = null; + int? uv3id = null; + int? uv4id = null; + + if ( + (uv1 is null && (uv2 is not null || uv3 is not null || uv4 is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null || uv4 is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null || uv4 is not null)) || + (uv4 is null && (uv1 is not null || uv2 is not null || uv3 is not null)) + ) + throw new InvalidDataException ("UVs must all be null or all have values"); + + if (uv1 is not null) + { + uv1id = this.uvs.Count; + uv2id = this.uvs.Count + 1; + uv3id = this.uvs.Count + 2; + uv4id = this.uvs.Count + 3; + + this.uvs.Add (uv1.Value); + this.uvs.Add (uv2.Value); + this.uvs.Add (uv3.Value); + this.uvs.Add (uv4.Value); + } + + this.faces.Add ( + new Face + { + material = material ?? DEFAULT_MATERIAL, + V1 = v1, + V2 = v2, + V3 = v3, + V4 = v4, + UV1 = uv1id, + UV2 = uv2id, + UV3 = uv3id, + UV4 = uv4id + } + ); + } + + /// + /// Creates a new face with it's own vertices and uv coordinates + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void AddFace (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 c1, Vector3 c2, Vector3 c3, string material = null, Vector2? uv1 = null, Vector2? uv2 = null, Vector2? uv3 = null) + { + int v1id = this.vertices.Count; + int v2id = this.vertices.Count + 1; + int v3id = this.vertices.Count + 2; + int? uv1id = null; + int? uv2id = null; + int? uv3id = null; + + if ( + (uv1 is null && (uv2 is not null || uv3 is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null)) + ) + throw new InvalidDataException ("UVs must all be null or all have values"); + + if (uv1 is not null) + { + uv1id = this.uvs.Count; + uv2id = this.uvs.Count + 1; + uv3id = this.uvs.Count + 2; + + this.uvs.Add (uv1.Value); + this.uvs.Add (uv2.Value); + this.uvs.Add (uv3.Value); + } + + this.vertices.Add ( + new Vertex + { + position = v1, + color = c1 + } + ); + this.vertices.Add ( + new Vertex + { + position = v2, + color = c2 + } + ); + this.vertices.Add ( + new Vertex + { + position = v3, + color = c3 + } + ); + + this.faces.Add ( + new Face + { + material = material, + V1 = v1id, + V2 = v2id, + V3 = v3id, + UV1 = uv1id, + UV2 = uv2id, + UV3 = uv3id + } + ); + } + + /// + /// Creates a new face with it's own vertices and uv coordinates + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void AddFace (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 c1, Vector3 c2, Vector3 c3, Vector3 c4, string material = null, Vector2? uv1 = null, Vector2? uv2 = null, Vector2? uv3 = null, Vector2? uv4 = null) + { + int v1id = this.vertices.Count; + int v2id = this.vertices.Count + 1; + int v3id = this.vertices.Count + 2; + int v4id = this.vertices.Count + 3; + int? uv1id = null; + int? uv2id = null; + int? uv3id = null; + int? uv4id = null; + + if ( + (uv1 is null && (uv2 is not null || uv3 is not null || uv4 is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null || uv4 is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null || uv4 is not null)) || + (uv4 is null && (uv1 is not null || uv2 is not null || uv3 is not null)) + ) + throw new InvalidDataException ("UVs must all be null or all have values"); + + if (uv1 is not null) + { + uv1id = this.uvs.Count; + uv2id = this.uvs.Count + 1; + uv3id = this.uvs.Count + 2; + uv4id = this.uvs.Count + 3; + + this.uvs.Add (uv1.Value); + this.uvs.Add (uv2.Value); + this.uvs.Add (uv3.Value); + this.uvs.Add (uv4.Value); + } + + this.vertices.Add ( + new Vertex + { + position = v1, + color = c1 + } + ); + this.vertices.Add ( + new Vertex + { + position = v2, + color = c2 + } + ); + this.vertices.Add ( + new Vertex + { + position = v3, + color = c3 + } + ); + this.vertices.Add ( + new Vertex + { + position = v4, + color = c4 + } + ); + + this.faces.Add ( + new Face + { + material = material, + V1 = v1id, + V2 = v2id, + V3 = v3id, + V4 = v4id, + UV1 = uv1id, + UV2 = uv2id, + UV3 = uv3id, + UV4 = uv4id + } + ); + } + + private void ExportMaterials (string path, string modelname) + { + // first write all the textures to disk + // then write the mtl file + using MemoryStream stream = new MemoryStream (); + using StreamWriter writer = new StreamWriter (stream); + + writer.WriteLine ("# CrashEdit exported material"); + + // write all the materials + foreach (KeyValuePair material in this.materials) + { + writer.WriteLine ("newmtl {0}", material.Key); + writer.WriteLine ( + "Ka {0} {1} {2}", + material.Value.ambient.X.ToString(CultureInfo.InvariantCulture), + material.Value.ambient.Y.ToString(CultureInfo.InvariantCulture), + material.Value.ambient.Z.ToString(CultureInfo.InvariantCulture) + ); + writer.WriteLine ( + "Kd {0} {1} {2}", + material.Value.diffuse.X.ToString(CultureInfo.InvariantCulture), + material.Value.diffuse.Y.ToString(CultureInfo.InvariantCulture), + material.Value.diffuse.Z.ToString(CultureInfo.InvariantCulture) + ); + writer.WriteLine ( + "Ks {0} {1} {2}", + material.Value.specular.X.ToString(CultureInfo.InvariantCulture), + material.Value.specular.Y.ToString(CultureInfo.InvariantCulture), + material.Value.specular.Z.ToString(CultureInfo.InvariantCulture) + ); + writer.WriteLine ( + "Ns {0}", + material.Value.highlight.ToString(CultureInfo.InvariantCulture) + ); + + if (material.Value.texture is null) + continue; + + writer.WriteLine( + "map_Kd {0}.bmp", + material.Key + ); + + // write the bitmap to a file too + material.Value.texture.Save (path + Path.DirectorySeparatorChar + material.Key + ".bmp"); + } + + writer.Flush (); + + // material file finally written, save it to disk too + File.WriteAllBytes (path + Path.DirectorySeparatorChar + modelname + ".mtl", stream.ToArray ()); + } + + public void Export (string path, string modelname) + { + // first write the material file + ExportMaterials (path, modelname); + + using MemoryStream stream = new MemoryStream(); + using StreamWriter writer = new StreamWriter (stream); + + writer.WriteLine ("# CrashEdit exported model"); + writer.WriteLine ("mtllib {0}.mtl", modelname); + writer.WriteLine ("# Vertices"); + + foreach (Vertex vertex in vertices) + { + writer.WriteLine ( + "v {0} {1} {2} {3} {4} {5}", + vertex.position.X.ToString(CultureInfo.InvariantCulture), + vertex.position.Y.ToString(CultureInfo.InvariantCulture), + vertex.position.Z.ToString(CultureInfo.InvariantCulture), + vertex.color.X.ToString(CultureInfo.InvariantCulture), + vertex.color.Y.ToString(CultureInfo.InvariantCulture), + vertex.color.Z.ToString(CultureInfo.InvariantCulture) + ); + } + + // write any uvs we have + writer.WriteLine (); + writer.WriteLine ("# UVs"); + + foreach (Vector2 uv in uvs) + { + writer.WriteLine( + "vt {0} {1}", + uv.X.ToString(CultureInfo.InvariantCulture), uv.Y.ToString(CultureInfo.InvariantCulture) + ); + } + + // finally write the faces + writer.WriteLine (); + writer.WriteLine ("# Faces with textures"); + + string lastmaterial = null; + + // by default use the default material + writer.WriteLine ("usemtl {0}", DEFAULT_MATERIAL); + + foreach (Face face in faces.OrderBy (x => x.material)) + { + if (lastmaterial != face.material) + { + writer.WriteLine ("usemtl {0}", face.material); + + lastmaterial = face.material; + } + + // write face information, UVs must all be null or have value + // at the same time, so this check is safe + if (face.UV1 is null) + { + if (face.V4 is null) + { + writer.WriteLine ( + "f {0} {1} {2}", + face.V1 + 1, + face.V2 + 1, + face.V3 + 1 + ); + } + else + { + writer.WriteLine ( + "f {0} {1} {2} {3}", + face.V1 + 1, + face.V2 + 1, + face.V3 + 1, + face.V4 + 1 + ); + } + } + else + { + if (face.V4 is null) + { + writer.WriteLine ( + "f {0}/{3} {1}/{4} {2}/{5}", + face.V1 + 1, + face.V2 + 1, + face.V3 + 1, + face.UV1 + 1, + face.UV2 + 1, + face.UV3 + 1 + ); + } + else + { + writer.WriteLine ( + "f {0}/{4} {1}/{5} {2}/{6} {3}/{7}", + face.V1 + 1, + face.V2 + 1, + face.V3 + 1, + face.V4 + 1, + face.UV1 + 1, + face.UV2 + 1, + face.UV3 + 1, + face.UV4 + 1 + ); + } + } + } + + writer.Flush (); + + // obj file ready, write to the destination + File.WriteAllBytes (path + Path.DirectorySeparatorChar + modelname + ".obj", stream.ToArray ()); + } +} \ No newline at end of file diff --git a/CrashEdit/Exporters/SceneryExtensions.cs b/CrashEdit/Exporters/SceneryExtensions.cs new file mode 100644 index 00000000..d8b486ac --- /dev/null +++ b/CrashEdit/Exporters/SceneryExtensions.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using Crash; +using OpenTK; + +namespace CrashEdit.Exporters; + +// TODO: ALL SCENERY CLASSES/STRUCTS SHOULD HAVE A BASE INTERFACE WITH DATA IN COMMON +// TODO: THAT WOULD MAKE WORKING WITH THEM EASIER FOR THINGS LIKE THESE WHERE YOU ONLY NEED THE DATA +// TODO: THEY HAVE IN COMMON, BUT CHANGING THAT IS OUT OF THE SCOPE OF THESE COMMITS +// TODO: BUT THAT WOULD CUT DOWN THE METHODS HERE TO JUST ONE OR TWO +public static class SceneryExtensions +{ + public static void AddScenery (this OBJExporter exporter, NSF nsf, OldSceneryEntry scenery, ref Dictionary textureEIDs, ref Dictionary objTranslate) + { + var offset = new Vector3 (scenery.XOffset, scenery.YOffset, scenery.ZOffset); + var scale = new Vector3 (1 / GameScales.WorldC1); + + for (int i = 0; i < scenery.TPAGCount; i++) + { + int tpag_eid = scenery.GetTPAG (i); + + if (textureEIDs.ContainsKey (tpag_eid)) + continue; + + textureEIDs.Add (tpag_eid, textureEIDs.Count); + } + + foreach (var polygon in scenery.Polygons) + { + string material = null; + Vector2? uv1 = null, uv2 = null, uv3 = null; + OldModelStruct str = scenery.Structs[polygon.ModelStruct]; + OldSceneryVertex ov1 = scenery.Vertices [polygon.VertexA]; + OldSceneryVertex ov2 = scenery.Vertices [polygon.VertexB]; + OldSceneryVertex ov3 = scenery.Vertices [polygon.VertexC]; + Vector3 v1 = new Vector3 (ov1.X, ov1.Y, ov1.Z); + Vector3 v2 = new Vector3 (ov2.X, ov2.Y, ov2.Z); + Vector3 v3 = new Vector3 (ov3.X, ov3.Y, ov3.Z); + Vector3 color = Vector3.Zero; + + if (str is OldSceneryTexture t) + { + int textureEID = scenery.GetTPAG (polygon.Page); + material = exporter.AddTexture (nsf, t, textureEID, ref textureEIDs, ref objTranslate, out color, out uv1, out uv2, out uv3); + } + else if(str is OldSceneryColor c) + { + color = new Vector3 (c.R, c.G, c.B) / 255F; + } + + exporter.AddFace ( + (v1 + offset) * scale, + (v2 + offset) * scale, + (v3 + offset) * scale, + color, color, color, + material, + uv1, uv2, uv3 + ); + } + } + + public static void AddScenery (this OBJExporter exporter, NSF nsf, SceneryEntry scenery, ref Dictionary textureEIDs, ref Dictionary objTranslate) + { + var offset = new Vector3 (scenery.XOffset, scenery.YOffset, scenery.ZOffset); + //var scale = new Vector3 (1 / GameScales.WorldC1); + + for (int i = 0; i < scenery.TPAGCount; i++) + { + int tpag_eid = scenery.GetTPAG (i); + + if (textureEIDs.ContainsKey (tpag_eid)) + continue; + + textureEIDs.Add (tpag_eid, textureEIDs.Count); + } + + foreach (var tri in scenery.Triangles) + { + if (tri.VertexA > scenery.Vertices.Count || + tri.VertexB > scenery.Vertices.Count || + tri.VertexC > scenery.Vertices.Count) + continue; + + Vector2? uv1 = null, uv2 = null, uv3 = null; + + string material = exporter.AddTexture (nsf, tri, scenery, ref textureEIDs, ref objTranslate, out uv1, out uv2, out uv3); + + // add the face + SceneryVertex fv1 = scenery.Vertices [tri.VertexA]; + SceneryVertex fv2 = scenery.Vertices [tri.VertexB]; + SceneryVertex fv3 = scenery.Vertices [tri.VertexC]; + SceneryColor fc1 = scenery.Colors [fv1.Color]; + SceneryColor fc2 = scenery.Colors [fv2.Color]; + SceneryColor fc3 = scenery.Colors [fv3.Color]; + Vector3 v1 = new Vector3 (fv1.X, fv1.Y, fv1.Z); + Vector3 v2 = new Vector3 (fv2.X, fv2.Y, fv2.Z); + Vector3 v3 = new Vector3 (fv3.X, fv3.Y, fv3.Z); + Vector3 c1 = new Vector3 (fc1.Red, fc1.Green, fc1.Blue) / 255f; + Vector3 c2 = new Vector3 (fc2.Red, fc2.Green, fc2.Blue) / 255f; + Vector3 c3 = new Vector3 (fc3.Red, fc3.Green, fc3.Blue) / 255f; + + exporter.AddFace ( + (v1 * 16 + offset) / GameScales.WorldC1, + (v2 * 16 + offset) / GameScales.WorldC1, + (v3 * 16 + offset) / GameScales.WorldC1, + c1, c2, c3, + material, + uv1, uv2, uv3 + ); + } + + foreach (var quad in scenery.Quads) + { + // ignore quads that are out of limits + if (quad.VertexA > scenery.Vertices.Count || + quad.VertexB > scenery.Vertices.Count || + quad.VertexC > scenery.Vertices.Count || + quad.VertexD > scenery.Vertices.Count) + continue; + + Vector2? uv1 = null, uv2 = null, uv3 = null, uv4 = null; + string material = exporter.AddTexture (nsf, quad, scenery, ref textureEIDs, ref objTranslate, out uv1, out uv2, out uv3, out uv4); + + // add the face + SceneryVertex fv1 = scenery.Vertices [quad.VertexA]; + SceneryVertex fv2 = scenery.Vertices [quad.VertexB]; + SceneryVertex fv3 = scenery.Vertices [quad.VertexC]; + SceneryVertex fv4 = scenery.Vertices [quad.VertexD]; + SceneryColor fc1 = scenery.Colors [fv1.Color]; + SceneryColor fc2 = scenery.Colors [fv2.Color]; + SceneryColor fc3 = scenery.Colors [fv3.Color]; + SceneryColor fc4 = scenery.Colors [fv4.Color]; + Vector3 v1 = new Vector3 (fv1.X, fv1.Y, fv1.Z); + Vector3 v2 = new Vector3 (fv2.X, fv2.Y, fv2.Z); + Vector3 v3 = new Vector3 (fv3.X, fv3.Y, fv3.Z); + Vector3 v4 = new Vector3 (fv4.X, fv4.Y, fv4.Z); + Vector3 c1 = new Vector3 (fc1.Red, fc1.Green, fc1.Blue) / 255f; + Vector3 c2 = new Vector3 (fc2.Red, fc2.Green, fc2.Blue) / 255f; + Vector3 c3 = new Vector3 (fc3.Red, fc3.Green, fc3.Blue) / 255f; + Vector3 c4 = new Vector3 (fc4.Red, fc4.Green, fc4.Blue) / 255f; + + exporter.AddFace ( + (v1 * 16 + offset) / GameScales.WorldC1, + (v2 * 16 + offset) / GameScales.WorldC1, + (v3 * 16 + offset) / GameScales.WorldC1, + (v4 * 16 + offset) / GameScales.WorldC1, + c1, c2, c3, c4, + material, + uv1, uv2, uv3, uv4 + ); + } + } +} \ No newline at end of file diff --git a/CrashEdit/Exporters/TextureExporter.cs b/CrashEdit/Exporters/TextureExporter.cs new file mode 100644 index 00000000..51b73a22 --- /dev/null +++ b/CrashEdit/Exporters/TextureExporter.cs @@ -0,0 +1,173 @@ +using System.Drawing; + +namespace CrashEdit.Exporters; + +public class TextureExporter +{ + public static Bitmap Create4bpp (byte [] data, TexInfoUnpacked info) + { + Bitmap bmp = new Bitmap (1024, 128); + + // clutx really contains the count of palettes available + // so multiplying by 16 gives the right amount of info for it + int clutx = info.clutx * 16; + + // write the texture somewhere for now, testing + for (int x = 0; x < 1024; x++) + { + for (int y = info.cluty; y < 128; y++) + { + byte entry = data [y * 512 + (x / 2)]; + + if ((x % 2) == 0) + entry = (byte) (entry & 0xF); + else + entry = (byte) ((entry >> 4) & 0xF); + + // now read the color from the palette + ushort color = (ushort) ( + data [info.cluty * 512 + ((clutx + entry) * 2)] | + (data [info.cluty * 512 + ((clutx + entry) * 2 + 1)] << 8) + ); + + int red = (int) (((color & 0x1F) / 31F) * 255); + int green = (int) ((((color >> 5) & 0x1F) / 31F) * 255); + int blue = (int) ((((color >> 10) & 0x1F) / 31F) * 255); + int a = ((color >> 15) & 0x1); + int alpha = 255; + + if (info.blend == 0 && (info.color == 0 || info.color == 1)) + { + if (a == 1) + alpha = 127; + else + alpha = 255; + + if (red == 0 && green == 0 && blue == 0 && a == 0) + alpha = 0; + } + else if (info.blend == 1 && (info.color == 0 || info.color == 1)) + { + if (red == 0 && green == 0 && blue == 0 && a == 0) + alpha = 0; + } + + // colors are in rgb15 format + Color c = Color.FromArgb ( + alpha, + red, + green, + blue + ); + + bmp.SetPixel (x, y, c); + } + } + + return bmp; + } + + public static Bitmap Create8bpp (byte [] data, TexInfoUnpacked info) + { + Bitmap bmp = new Bitmap (512, 128); + + // clutx really contains the count of palettes available + // so multiplying by 16 gives the right amount of info for it + int clutx = info.clutx * 16; + + // write the texture somewhere for now, testing + for (int x = 0; x < 512; x++) + { + for (int y = info.cluty; y < 128; y++) + { + byte entry = data [y * 512 + x]; + + // now read the color from the palette + ushort color = (ushort) ( + data [info.cluty * 512 + ((clutx + entry) * 2)] | + (data [info.cluty * 512 + ((clutx + entry) * 2 + 1)] << 8) + ); + + int red = (int) (((color & 0x1F) / 31F) * 255); + int green = (int) ((((color >> 5) & 0x1F) / 31F) * 255); + int blue = (int) ((((color >> 10) & 0x1F) / 31F) * 255); + int a = ((color >> 15) & 0x1); + int alpha = 255; + + if (info.blend == 0 && (info.color == 0 || info.color == 1)) + { + if (a == 1) + alpha = 127; + else + alpha = 255; + + if (red == 0 && green == 0 && blue == 0 && a == 0) + alpha = 0; + } + else if (info.blend == 1 && (info.color == 0 || info.color == 1)) + { + if (red == 0 && green == 0 && blue == 0 && a == 0) + alpha = 0; + } + + // colors are in rgb15 format + Color c = Color.FromArgb ( + alpha, + red, + green, + blue + ); + + bmp.SetPixel (x, y, c); + } + } + + return bmp; + } + + public static Bitmap Create16bpp (byte [] data, TexInfoUnpacked info) + { + Bitmap bmp = new Bitmap (256, 128); + + // clutx really contains the count of palettes available + // so multiplying by 16 gives the right amount of info for it + int clutx = info.clutx * 16; + + // write the texture somewhere for now, testing + for (int x = 0; x < 256; x++) + { + for (int y = info.cluty; y < 128; y++) + { + // now read the color from the palette + ushort color = (ushort) ( + data [(info.cluty + y) * 512 + ((clutx + x) * 2)] | + (data [(info.cluty + y) * 512 + ((clutx + x) * 2 + 1)] << 8) + ); + + // this is not 100% precise, but we can fix these manually + // as they're usually not that big + // colors are in rgb15 format + Color c = Color.FromArgb ( + ((color >> 15) & 0x1) * 255, + (int) (((color & 0x1F) / 31F) * 255), + (int) ((((color >> 5) & 0x1F) / 31F) * 255), + (int) ((((color >> 10) & 0x1F) / 31F) * 255) + ); + + bmp.SetPixel (x, y, c); + } + } + + return bmp; + } + + public static Bitmap CreateTexture (byte [] data, TexInfoUnpacked info) + { + return info.color switch + { + 0 => TextureExporter.Create4bpp (data, info), + 1 => TextureExporter.Create8bpp (data, info), + _ => TextureExporter.Create16bpp (data, info) + }; + } +} \ No newline at end of file diff --git a/CrashEdit/FileUtil.cs b/CrashEdit/FileUtil.cs index 94cb7492..728e7bb1 100755 --- a/CrashEdit/FileUtil.cs +++ b/CrashEdit/FileUtil.cs @@ -50,6 +50,29 @@ public static byte[][] OpenFiles(params string[] filters) } } + /// + /// Allows the user to select a path to save a file but leaves the actual file saving to the caller + /// + /// Useful for batch exports of OBJ files + /// + /// The filename to write as + /// + /// If the user selected a file to save + public static bool SelectSaveFile (out string filename, params string [] filters) + { + savefiledlg.Filter = string.Join("|", filters); + if (savefiledlg.ShowDialog(Owner) == DialogResult.OK) + { + filename = savefiledlg.FileName; + return true; + } + else + { + filename = null; + return false; + } + } + public static bool SaveFile(byte[] data, params string[] filters) { if (data == null) diff --git a/CrashEdit/Utils/TextureUtils.cs b/CrashEdit/Utils/TextureUtils.cs new file mode 100644 index 00000000..2ead55ed --- /dev/null +++ b/CrashEdit/Utils/TextureUtils.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Crash; + +namespace CrashEdit; + +public class TextureUtils +{ + public static Tuple ProcessTextureInfoC2(long currentFrame, int in_tex_id, bool animated, IList textures, IList animated_textures) + { + if (in_tex_id != 0 || animated) + { + ModelTexture? info_temp = null; + int tex_id = in_tex_id - 1; + if (animated) + { + if (++tex_id >= animated_textures.Count) + { + return new(false, null); + } + var anim = animated_textures[tex_id]; + // check if it's an untextured polygon + if (anim.Offset != 0) + { + tex_id = anim.Offset - 1; + if (anim.IsLOD) + { + tex_id += anim.LOD0; // we only render closest LOD for now + } + else + { + tex_id += (int)((currentFrame / 2 / (1 + anim.Latency) + anim.Delay) & anim.Mask); + if (anim.Leap) + { + anim = animated_textures[++tex_id]; + tex_id = anim.Offset - 1 + anim.LOD0; + } + } + if (tex_id >= textures.Count) + { + return new(false, null); + } + info_temp = textures[tex_id]; + } + } + else + { + if (tex_id >= textures.Count) + { + return new(false, null); + } + info_temp = textures[tex_id]; + } + return new(true, info_temp); + } + return new(true, null); + } +} \ No newline at end of file