diff --git a/Crash.UI/Properties/Resources.Designer.cs b/Crash.UI/Properties/Resources.Designer.cs index 1ece3293..717c190d 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 index 68fe29a9..0c297980 100755 --- a/Crash/Formats/Crash Formats/Animation/OldFrame.cs +++ b/Crash/Formats/Crash Formats/Animation/OldFrame.cs @@ -128,46 +128,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 index 2cd0b068..c71f48e1 100755 --- 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 (NewSceneryVertex 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 (NewSceneryVertex 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.Main.csproj b/CrashEdit.Main.csproj index ea0c32eb..411a423f 100644 --- a/CrashEdit.Main.csproj +++ b/CrashEdit.Main.csproj @@ -4,6 +4,8 @@ net8.0-windows enable enable + false + false diff --git a/CrashEdit/Controllers/Animation/AnimationEntryController.cs b/CrashEdit/Controllers/Animation/AnimationEntryController.cs index 14dc1aa7..af6e1085 100644 --- a/CrashEdit/Controllers/Animation/AnimationEntryController.cs +++ b/CrashEdit/Controllers/Animation/AnimationEntryController.cs @@ -8,6 +8,7 @@ public sealed class AnimationEntryController : EntryController public AnimationEntryController(AnimationEntry animationentry, SubcontrollerGroup parentGroup) : base(animationentry, parentGroup) { AnimationEntry = animationentry; + AddMenu ("Export as OBJ", Menu_Export_OBJ); } public override bool EditorAvailable => true; @@ -18,5 +19,28 @@ public override Control CreateEditor() } public AnimationEntry AnimationEntry { get; } + + 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 = Modern.SubcontrollerGroups.SelectMany(x => x.Members).Count().ToString().Length; + + foreach (Controller node in Modern.SubcontrollerGroups.SelectMany (x => x.Members)) + { + if (node.Legacy is not FrameController frame) + continue; + + frame.ToOBJ (path, filename + id.ToString().PadLeft (count, '0')); + id++; + } + } } } diff --git a/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs b/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs index 276ad1f2..5a533eea 100644 --- a/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs +++ b/CrashEdit/Controllers/Animation/ColoredAnimationEntryController.cs @@ -9,6 +9,7 @@ public ColoredAnimationEntryController(ColoredAnimationEntry coloredanimationent : base(coloredanimationentry, parentGroup) { ColoredAnimationEntry = coloredanimationentry; + AddMenu ("Export as OBJ", Menu_Export_OBJ); } public override bool EditorAvailable => true; @@ -19,6 +20,29 @@ public override Control CreateEditor() } public ColoredAnimationEntry ColoredAnimationEntry { get; } + + 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 = Modern.SubcontrollerGroups.SelectMany(x => x.Members).Count().ToString().Length; + + foreach (Controller node in Modern.SubcontrollerGroups.SelectMany (x => x.Members)) + { + if (node.Legacy is not FrameController frame) + continue; + + frame.ToOBJ (path, filename + id.ToString().PadLeft (count, '0')); + id++; + } + } } } diff --git a/CrashEdit/Controllers/Animation/ColoredFrameController.cs b/CrashEdit/Controllers/Animation/ColoredFrameController.cs new file mode 100644 index 00000000..e69de29b diff --git a/CrashEdit/Controllers/Animation/FrameController.cs b/CrashEdit/Controllers/Animation/FrameController.cs index 1b430a25..166c83fd 100644 --- a/CrashEdit/Controllers/Animation/FrameController.cs +++ b/CrashEdit/Controllers/Animation/FrameController.cs @@ -1,4 +1,7 @@ +using System.Globalization; using CrashEdit.Crash; +using CrashEdit.Exporters; +using OpenTK.Mathematics; namespace CrashEdit.CE { @@ -8,6 +11,7 @@ public sealed class FrameController : LegacyController public FrameController(Frame frame, SubcontrollerGroup parentGroup) : base(parentGroup, frame) { Frame = frame; + AddMenu ("Export as OBJ", Menu_Export_OBJ); } public override bool EditorAvailable => true; @@ -19,5 +23,34 @@ public override Control CreateEditor() public AnimationEntryController AnimationEntryController => (AnimationEntryController)Modern.Parent.Legacy; public Frame Frame { get; } + + + 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 (this.GetNSF (), Frame, ref textureEIDs, ref objTranslate); + exporter.Export (path, modelname); + } } } diff --git a/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs b/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs index 3231fe75..2fbfe5d3 100755 --- a/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs +++ b/CrashEdit/Controllers/Animation/OldAnimationEntryController.cs @@ -8,6 +8,7 @@ public sealed class OldAnimationEntryController : EntryController public OldAnimationEntryController(OldAnimationEntry oldanimationentry, SubcontrollerGroup parentGroup) : base(oldanimationentry, parentGroup) { OldAnimationEntry = oldanimationentry; + AddMenu ("Export as OBJ", Menu_Export_OBJ); } public override bool EditorAvailable => true; @@ -18,5 +19,28 @@ public override Control CreateEditor() } public OldAnimationEntry OldAnimationEntry { get; } + + 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 = Modern.SubcontrollerGroups.SelectMany(x => x.Members).Count().ToString().Length; + + foreach (Controller node in Modern.SubcontrollerGroups.SelectMany (x => x.Members)) + { + if (node.Legacy is not OldFrameController frame) + continue; + + frame.ToOBJ (path, filename + id.ToString().PadLeft (count, '0')); + id++; + } + } } } diff --git a/CrashEdit/Controllers/Animation/OldFrameController.cs b/CrashEdit/Controllers/Animation/OldFrameController.cs index 363f6604..71ac4d1c 100755 --- a/CrashEdit/Controllers/Animation/OldFrameController.cs +++ b/CrashEdit/Controllers/Animation/OldFrameController.cs @@ -1,4 +1,7 @@ +using System.Globalization; using CrashEdit.Crash; +using CrashEdit.Exporters; +using OpenTK.Mathematics; namespace CrashEdit.CE { @@ -8,7 +11,7 @@ public sealed class OldFrameController : LegacyController public OldFrameController(OldFrame oldframe, SubcontrollerGroup parentGroup) : base(parentGroup, oldframe) { OldFrame = oldframe; - AddMenu("Export as OBJ", Menu_Export_OBJ); + AddMenu ("Export as OBJ", Menu_Export_OBJ); } public override bool EditorAvailable => true; @@ -52,11 +55,21 @@ private void Menu_Export_OBJ() { 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 (this.GetNSF (), OldFrame, ref textureEIDs, ref objTranslate); + exporter.Export (path, modelname); } } } diff --git a/CrashEdit/Controllers/NSFController.cs b/CrashEdit/Controllers/NSFController.cs index 044b39b7..4b2fff3f 100755 --- a/CrashEdit/Controllers/NSFController.cs +++ b/CrashEdit/Controllers/NSFController.cs @@ -1,4 +1,5 @@ using CrashEdit.Crash; +using CrashEdit.Exporters; namespace CrashEdit.CE { @@ -26,6 +27,7 @@ public NSFController(NSF nsf, SubcontrollerGroup parentGroup) : base(parentGroup { AddMenu(CrashUI.Properties.Resources.NSFController_AcShowLevel, Menu_ShowLevelC1); AddMenu(CrashUI.Properties.Resources.NSFController_AcShowLevelZones, Menu_ShowLevelZonesC1); + AddMenu(CrashUI.Properties.Resources.NSFController_AcExportScenery, Menu_ExportSceneryC1OBJ); } else if (GameVersion == GameVersion.Crash1Beta1995) { @@ -34,8 +36,11 @@ public NSFController(NSF nsf, SubcontrollerGroup parentGroup) : base(parentGroup } else if (GameVersion == GameVersion.Crash2 || GameVersion == GameVersion.Crash3) { + AddMenuSeparator(); AddMenu(CrashUI.Properties.Resources.NSFController_AcShowLevel, Menu_ShowLevelC2); AddMenu(CrashUI.Properties.Resources.NSFController_AcShowLevelZones, Menu_ShowLevelZonesC2); + AddMenuSeparator (); + AddMenu(CrashUI.Properties.Resources.NSFController_AcExportScenery, Menu_ExportSceneryC2OBJ); } } @@ -308,6 +313,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 2a98f694..3d37547f 100755 --- a/CrashEdit/Controllers/Scenery/OldSceneryEntryController.cs +++ b/CrashEdit/Controllers/Scenery/OldSceneryEntryController.cs @@ -1,4 +1,5 @@ using CrashEdit.Crash; +using CrashEdit.Exporters; namespace CrashEdit.CE { @@ -10,7 +11,6 @@ public OldSceneryEntryController(OldSceneryEntry oldsceneryentry, SubcontrollerG OldSceneryEntry = oldsceneryentry; AddMenuSeparator(); AddMenu("Export as OBJ", Menu_Export_OBJ); - AddMenu("Export as COLLADA", Menu_Export_COLLADA); } public override bool EditorAvailable => true; @@ -24,20 +24,20 @@ public override Control CreateEditor() private void Menu_Export_OBJ() { - if (MessageBox.Show("Exporting to 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(OldSceneryEntry.ToOBJ(), FileFilters.OBJ, FileFilters.Any); + + ToOBJ (Path.GetDirectoryName (filename), Path.GetFileNameWithoutExtension (filename)); } - private void Menu_Export_COLLADA() + private void ToOBJ (string path, string modelname) { - 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); + var exporter = new OBJExporter (); + Dictionary textureEIDs = new (); + Dictionary objTranslate = new Dictionary (); + + exporter.AddScenery (this.GetNSF (), OldSceneryEntry, ref textureEIDs, ref objTranslate); + exporter.Export (path, modelname); } } } diff --git a/CrashEdit/Controllers/Scenery/SceneryEntryController.cs b/CrashEdit/Controllers/Scenery/SceneryEntryController.cs index 79e8be6c..5db83967 100755 --- a/CrashEdit/Controllers/Scenery/SceneryEntryController.cs +++ b/CrashEdit/Controllers/Scenery/SceneryEntryController.cs @@ -1,4 +1,6 @@ using CrashEdit.Crash; +using CrashEdit.Exporters; +using OpenTK.Mathematics; namespace CrashEdit.CE { @@ -10,7 +12,6 @@ public SceneryEntryController(SceneryEntry sceneryentry, SubcontrollerGroup pare 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); } @@ -24,33 +25,24 @@ public 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) - { - return; - } - FileUtil.SaveFile(SceneryEntry.ToPLY(), FileFilters.PLY, FileFilters.Any); + var exporter = new OBJExporter (); + Dictionary textureEIDs = new (); + Dictionary objTranslate = new Dictionary (); + + exporter.AddScenery (this.GetNSF (), SceneryEntry, ref textureEIDs, ref objTranslate); + exporter.Export (path, modelname); } - - /*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) - { - return; - } - FileUtil.SaveFile(sceneryentry.ToCOLLADA(), FileFilters.COLLADA, FileFilters.Any); - }*/ - + private void Menu_Fix_WGEOv3() { for (int i = 0; i < SceneryEntry.Vertices.Count; i++) diff --git a/CrashEdit/Controls/3D/GLViewer.cs b/CrashEdit/Controls/3D/GLViewer.cs index b577b369..f3010795 100644 --- a/CrashEdit/Controls/3D/GLViewer.cs +++ b/CrashEdit/Controls/3D/GLViewer.cs @@ -1096,66 +1096,7 @@ protected void UploadTPAGs() } } } - - protected bool ProcessTextureInfoC2(int in_tex_id, bool animated, IList textures, IList animated_textures, out ModelTexture tex) => ProcessTextureInfoC2(render.RealCurrentFrame / 2, in_tex_id, animated, textures, animated_textures, out tex); - - public static bool ProcessTextureInfoC2(long texture_frame, int in_tex_id, bool animated, IList textures, IList animated_textures, out ModelTexture tex) - { - if (in_tex_id != 0 || animated) - { - int tex_id = in_tex_id - 1; - if (animated) - { - if (++tex_id >= animated_textures.Count) - { - tex = default; - return false; - } - 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)((texture_frame / (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) - { - tex = default; - return false; - } - tex = textures[tex_id]; - } - else - { - tex = default; - } - } - else - { - if (tex_id >= textures.Count) - { - tex = default; - return false; - } - tex = textures[tex_id]; - } - return true; - } - tex = default; - return true; - } - + protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); diff --git a/CrashEdit/Controls/3D/SceneryEntryViewer.cs b/CrashEdit/Controls/3D/SceneryEntryViewer.cs index 135b503a..17ac439b 100644 --- a/CrashEdit/Controls/3D/SceneryEntryViewer.cs +++ b/CrashEdit/Controls/3D/SceneryEntryViewer.cs @@ -123,7 +123,7 @@ private void RenderTriangle(SceneryEntry world, int index) var tri = world.Triangles[index]; if (tri.VertexA >= world.Vertices.Count || tri.VertexB >= world.Vertices.Count || tri.VertexC >= world.Vertices.Count) return; - if (!ProcessTextureInfoC2(tri.Texture, tri.Animated, world.Textures, world.AnimatedTextures, out var polygon_texture_info)) + if (!TextureUtils.ProcessTextureInfoC2(this.render.CurrentFrame / 2, tri.Texture, tri.Animated, world.Textures, world.AnimatedTextures, out var polygon_texture_info)) return; ref var a = ref _vao.Verts[_vao.CurVert + 0]; ref var b = ref _vao.Verts[_vao.CurVert + 1]; @@ -153,7 +153,7 @@ private void RenderQuad(SceneryEntry world, int index, int state) var quad = world.Quads[index]; if (quad.VertexA >= world.Vertices.Count || quad.VertexB >= world.Vertices.Count || quad.VertexC >= world.Vertices.Count || quad.VertexD >= world.Vertices.Count) return; - if (!ProcessTextureInfoC2(quad.Texture, quad.Animated, world.Textures, world.AnimatedTextures, out var polygon_texture_info)) + if (!TextureUtils.ProcessTextureInfoC2(this.render.CurrentFrame / 2, quad.Texture, quad.Animated, world.Textures, world.AnimatedTextures, out var polygon_texture_info)) return; ref var a = ref _vao.Verts[_vao.CurVert + 0]; ref var b = ref _vao.Verts[_vao.CurVert + 1]; diff --git a/CrashEdit/Exporters/FrameExtensions.cs b/CrashEdit/Exporters/FrameExtensions.cs new file mode 100644 index 00000000..0b993c84 --- /dev/null +++ b/CrashEdit/Exporters/FrameExtensions.cs @@ -0,0 +1,128 @@ +using CrashEdit.CE; +using CrashEdit.Crash; +using OpenTK.Mathematics; + +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) + { + VertexTexInfo? material = null; + Vector2? uv1 = null, uv2 = null, uv3 = null; + OldModelStruct str = model.Structs [polygon.TexInfo]; + 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, t.EID, ref textureEIDs, ref objTranslate, out uv1, out uv2, out uv3); + color = new Vector3(t.R, t.G, t.B) / 255F; + } + 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) + { + VertexTexInfo? 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..35eaf26f --- /dev/null +++ b/CrashEdit/Exporters/MaterialExtensions.cs @@ -0,0 +1,151 @@ +using System.Runtime.CompilerServices; +using CrashEdit.CE; +using CrashEdit.Crash; +using OpenTK.Mathematics; + +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 VertexTexInfo? FindOrAddTexture + (this OBJExporter exporter, NSF nsf, int colorMode, int blendMode, int clutx, int cluty, short page, int face, int textureEID, ref Dictionary textureEIDs, ref Dictionary objTranslate) + { + VertexTexInfo? material = null; + + try + { + // ignore the texinfo if there's already a texture with the exact same settings stored + material = objTranslate.First (x => + x.Key.Color == colorMode && + x.Key.Blend == blendMode && + x.Key.ClutX == clutx && + x.Key.ClutY == cluty && + x.Key.Page == page && + x.Key.Face == face + ).Key; + } + catch (Exception e) + { + // first throw an exception if there's no match, thus here we create it + material = new VertexTexInfo ( + color: colorMode, blend: blendMode, clutx: clutx, cluty: cluty, page: page, face: face + ); + + var tpag = nsf.GetEntry (textureEIDs.First (x => x.Key == textureEID).Key); + + Bitmap texture = TextureExporter.CreateTexture (tpag.Data, material.Value); + + // the material name changes + exporter.AddTexture (material.Value, texture); + + // add it to the lookup table too + objTranslate.Add (material.Value, material.Value); + } + + return material; + } + + public static VertexTexInfo? 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) + { + VertexTexInfo? 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 (TextureUtils.ProcessTextureInfoC2 (0, tri.Texture, tri.Animated, model.Textures, model.AnimatedTextures, out ModelTexture value) && value is not null) + { + int textureEID = model.GetTPAG (value.Page); + + material = exporter.FindOrAddTexture (nsf, value.ColorMode, value.BlendMode, value.ClutX, value.ClutY, value.Page, 0, textureEID, ref textureEIDs, ref objTranslate); + + Vector2 texsize = TextureUtils.TextureSize (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 VertexTexInfo? 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) + { + VertexTexInfo? material = null; + uv1 = uv2 = uv3 = null; + + if (TextureUtils.ProcessTextureInfoC2 (0, tri.Texture, tri.Animated, scenery.Textures, scenery.AnimatedTextures, out ModelTexture value) && value is not null) + { + int textureEID = scenery.GetTPAG (value.Page); + + material = exporter.FindOrAddTexture (nsf, value.ColorMode, value.BlendMode, value.ClutX, value.ClutY, value.Page, 0, textureEID, ref textureEIDs, ref objTranslate); + + Vector2 texsize = TextureUtils.TextureSize (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); + } + + return material; + } + + public static VertexTexInfo? 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) + { + VertexTexInfo? material = null; + uv1 = uv2 = uv3 = uv4 = null; + + if (TextureUtils.ProcessTextureInfoC2 (0, quad.Texture, quad.Animated, scenery.Textures, scenery.AnimatedTextures, out ModelTexture value) && value is not null) + { + int textureEID = scenery.GetTPAG (value.Page); + + material = exporter.FindOrAddTexture (nsf, value.ColorMode, value.BlendMode, value.ClutX, value.ClutY, value.Page, 0, textureEID, ref textureEIDs, ref objTranslate); + + Vector2 texsize = TextureUtils.TextureSize (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 VertexTexInfo? AddTexture (this OBJExporter exporter, NSF nsf, OldSceneryTexture t, int textureEID, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3) + { + VertexTexInfo? material = exporter.FindOrAddTexture (nsf, t.ColorMode, t.BlendMode, t.ClutX, t.ClutY, (short) t.UVIndex, 0, textureEID, ref textureEIDs, ref objTranslate); + Vector2 texsize = TextureUtils.TextureSize (t.ColorMode); + + uv3 = new (t.U1 / texsize.X, (128 - t.V1) / texsize.Y); + uv2 = new (t.U2 / texsize.X, (128 - t.V2) / texsize.Y); + uv1 = new (t.U3 / texsize.X, (128 - t.V3) / texsize.Y); + + return material; + } + + public static VertexTexInfo? AddTexture (this OBJExporter exporter, NSF nsf, OldModelTexture t, int textureEID, ref Dictionary textureEIDs, ref Dictionary objTranslate, out Vector2? uv1, out Vector2? uv2, out Vector2? uv3) + { + VertexTexInfo? material = exporter.FindOrAddTexture (nsf, t.ColorMode, t.BlendMode, t.ClutX, t.ClutY, (short) t.UVIndex, Convert.ToInt32 (t.N), textureEID, ref textureEIDs, ref objTranslate); + Vector2 texsize = TextureUtils.TextureSize (t.ColorMode); + + uv3 = new (t.U3 / texsize.X, (128 - t.V3) / texsize.Y); + uv2 = new (t.U2 / texsize.X, (128 - t.V2) / texsize.Y); + uv1 = new (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..2346ec41 --- /dev/null +++ b/CrashEdit/Exporters/OBJExporter.cs @@ -0,0 +1,590 @@ +using System.Globalization; +using CrashEdit.CE; +using OpenTK.Mathematics; + +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; + } + + class Face + { + public int V1; + public int V2; + public int V3; + public int? V4; + public VertexTexInfo? texture; + public int? UV1; + public int? UV2; + public int? UV3; + public int? UV4; + } + + class Vertex + { + public Vector3 position; + public Vector3 color; + } + + class UV + { + public Vector2 position; + public VertexTexInfo texture; + } + + private Dictionary materials = new Dictionary (); + private List vertices = new List (); + private List faces = new List (); + private List uvs = new List (); + private Dictionary atlas = new Dictionary (); + + private static string BuildMaterialName (int color, int blending) => $"{DEFAULT_MATERIAL}c{color}b{blending}"; + + 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, + }; + + // create the rest of the atlas materials for proper rendering + for (int color = 0; color < 4; color++) + { + for (int blending = 0; blending < 4; blending++) + { + this.materials [BuildMaterialName (color, blending)] = new Material + { + ambient = Vector3.One, + diffuse = Vector3.One, + highlight = 0.0f, + specular = Vector3.Zero, + }; + } + } + } + + /// + /// Adds a texture with the given name to the obj + /// + /// Texture info + /// Texture data + /// The identifier for the texture in the obj export + public void AddTexture (VertexTexInfo texinfo, Bitmap texture) + { + string name = BuildMaterialName (texinfo.Color, texinfo.Blend); + + if (!this.atlas.TryGetValue (name, out TextureAtlas atlas)) + { + this.atlas[name] = atlas = new TextureAtlas (); + } + + atlas.AddTexture (texinfo, texture); + } + + /// + /// 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, VertexTexInfo? texture = 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 is null && (uv2 is not null || uv3 is not null || texture is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null || texture is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null || texture is not null)) || + (texture is null && (uv1 is not null || uv2 is not null || uv3 is not null)) + ) + throw new InvalidDataException ("UVs and texture must all be null or all have values"); + + if (uv1 is not null && uv2 is not null && uv3 is not null && texture is not null) + { + uv1id = this.uvs.Count; + uv2id = this.uvs.Count + 1; + uv3id = this.uvs.Count + 2; + + this.uvs.Add (new UV { position = uv1.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv2.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv3.Value, texture = texture.Value}); + } + + this.faces.Add ( + new Face + { + texture = texture, + 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, VertexTexInfo? texture = 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 || texture is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null || uv4 is not null || texture is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null || uv4 is not null || texture is not null)) || + (uv4 is null && (uv1 is not null || uv2 is not null || uv3 is not null || texture is not null)) || + (texture is null && (uv1 is not null || uv2 is not null || uv3 is not null || uv4 is not null)) + ) + throw new InvalidDataException ("UVs and texture must all be null or all have values"); + + if (uv1 is not null && uv2 is not null && uv3 is not null && uv4 is not null && texture 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 (new UV { position = uv1.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv2.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv3.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv4.Value, texture = texture.Value}); + } + + this.faces.Add ( + new Face + { + texture = texture, + 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, VertexTexInfo? texture = 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 || texture is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null || texture is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null || texture is not null)) || + (texture is null && (uv1 is not null || uv2 is not null || uv3 is not null)) + ) + throw new InvalidDataException ("UVs and texture must all be null or all have values"); + + if (uv1 is not null && uv2 is not null && uv3 is not null && texture is not null) + { + uv1id = this.uvs.Count; + uv2id = this.uvs.Count + 1; + uv3id = this.uvs.Count + 2; + + this.uvs.Add (new UV { position = uv1.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv2.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv3.Value, texture = texture.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 + { + texture = texture, + 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, VertexTexInfo? texture = 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 || texture is not null)) || + (uv2 is null && (uv1 is not null || uv3 is not null || uv4 is not null || texture is not null)) || + (uv3 is null && (uv1 is not null || uv2 is not null || uv4 is not null || texture is not null)) || + (uv4 is null && (uv1 is not null || uv2 is not null || uv3 is not null || texture is not null)) || + (texture is null && (uv1 is not null || uv2 is not null || uv3 is not null || uv4 is not null)) + ) + throw new InvalidDataException ("UVs must all be null or all have values"); + + if (uv1 is not null && uv2 is not null && uv3 is not null && uv4 is not null && texture 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 (new UV { position = uv1.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv2.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv3.Value, texture = texture.Value}); + this.uvs.Add (new UV { position = uv4.Value, texture = texture.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 + { + texture = texture, + V1 = v1id, + V2 = v2id, + V3 = v3id, + V4 = v4id, + UV1 = uv1id, + UV2 = uv2id, + UV3 = uv3id, + UV4 = uv4id + } + ); + } + + struct AtlasInfo + { + public Vector2 size; + public Dictionary pieces; + } + + private void ExportMaterials (string path, string modelname, out Dictionary topleft) + { + // first write all the textures to disk + // then write the mtl file + using MemoryStream stream = new MemoryStream (); + using StreamWriter writer = new StreamWriter (stream); + topleft = new Dictionary (); + + 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 (!this.atlas.TryGetValue (material.Key, out TextureAtlas atlas)) + continue; + + string filename = $"{modelname}_{material.Key}"; + + writer.WriteLine( + "map_Kd {0}.bmp", + filename + ); + + Dictionary topleftCurrent = new Dictionary (); + + // generate the bitmap based on the atlas + // TODO: ADD MODEL SAVENAME TO DIFFERENTIATE THE ATLAS? + atlas.BuildAtlas (out int width, out int height, out topleftCurrent).Save(path + Path.DirectorySeparatorChar + filename + ".bmp"); + + // add the textures to the information + topleft.Add (material.Key, new AtlasInfo () { size = new Vector2 (width, height), pieces = topleftCurrent}); + } + + 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, out Dictionary materialsTopleft); + + 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 (UV uv in uvs) + { + var topleft = materialsTopleft [BuildMaterialName (uv.texture.Color, uv.texture.Blend)]; + var piece = topleft.pieces.First (x => x.Key.Texinfo == uv.texture); + + // calculate new uv value + // these work from 0 to 1 + // 0,0 being the leftmost, top side, and 1,1 being the rightmost, bottom side + // as we've now got multiple textures in one + // these have to be recalculated from their slice texture to the global space + // for the model to export properly + Vector2 pieceOffset = piece.Value; + Vector2 actualPosition = new Vector2(uv.position.X, 1 - uv.position.Y) * new Vector2 (piece.Key.Data.Width, piece.Key.Data.Height) + pieceOffset; + Vector2 position = actualPosition / topleft.size; + position.Y = 1 - position.Y; + + writer.WriteLine( + "vt {0} {1}", + position.X.ToString(CultureInfo.InvariantCulture), + position.Y.ToString(CultureInfo.InvariantCulture) + ); + } + + // finally write the faces + writer.WriteLine (); + writer.WriteLine ("# Faces with textures"); + + VertexTexInfo? lastmaterial = null; + + // by default use the default material + writer.WriteLine ("usemtl {0}", DEFAULT_MATERIAL); + + foreach (Face face in faces.OrderBy (x => !x.texture.HasValue ? DEFAULT_MATERIAL : BuildMaterialName (x.texture.Value.Color, x.texture.Value.Blend))) + { + if ((!lastmaterial.HasValue && face.texture.HasValue) || + (lastmaterial.HasValue && face.texture.HasValue && (face.texture.Value.Color != lastmaterial.Value.Color || face.texture.Value.Blend != lastmaterial.Value.Blend))) + { + writer.WriteLine ("usemtl {0}", BuildMaterialName (face.texture.Value.Color, face.texture.Value.Blend)); + + lastmaterial = face.texture; + } + + // 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..ba2bc1e0 --- /dev/null +++ b/CrashEdit/Exporters/SceneryExtensions.cs @@ -0,0 +1,158 @@ +using CrashEdit.CE; +using CrashEdit.Crash; +using OpenTK.Mathematics; + +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) + { + VertexTexInfo? 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 c1 = Vector3.Zero; + Vector3 c2 = Vector3.Zero; + Vector3 c3 = Vector3.Zero; + + if (str is OldSceneryTexture t) + { + int textureEID = scenery.GetTPAG (polygon.Page); + material = exporter.AddTexture (nsf, t, textureEID, ref textureEIDs, ref objTranslate, out uv1, out uv2, out uv3); + c1 = new Vector3 (ov1.Red, ov1.Green, ov1.Blue) / 255F; + c2 = new Vector3 (ov2.Red, ov2.Green, ov2.Blue) / 255F; + c3 = new Vector3 (ov3.Red, ov3.Green, ov3.Blue) / 255F; + } + else if(str is OldSceneryColor c) + { + c1 = c2 = c3 = new Vector3 (c.R, c.G, c.B) / 255F; + } + + exporter.AddFace ( + (v1 + offset) * scale, + (v2 + offset) * scale, + (v3 + offset) * scale, + c1, c2, c3, + 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; + + VertexTexInfo? texture = 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, + texture, + 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; + VertexTexInfo? 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/TextureAtlas.cs b/CrashEdit/Exporters/TextureAtlas.cs new file mode 100644 index 00000000..4fd127ce --- /dev/null +++ b/CrashEdit/Exporters/TextureAtlas.cs @@ -0,0 +1,104 @@ +using CrashEdit.CE; +using OpenTK.Mathematics; + +namespace CrashEdit.Exporters; + +/// +/// Helper class that simplifies the creation of texture atlas +/// optimized for export +/// +public class TextureAtlas +{ + public class TextureInfo + { + public Bitmap Data; + public VertexTexInfo Texinfo; + } + // TODO: VertexTexInfo MIGHT NOT BE THE BEST TO DISCERN DIFFERENT TEXTURES + // TODO: MAINLY BECAUSE IT HAS MORE INFORMATION THAN JUST COLOR AND BLEND MODES + // TODO: AN OPTIMIZATION MIGHT BE ADJUSTING THIS TO TAKE THAT INTO ACCOUNT + // TODO: AND THUS REDUCING THE SIZE OF THE OUTPUT + + private Dictionary originals = new Dictionary (); + + /// + /// Registers the info of a texture for the atlas to be used + /// + /// + /// + public void AddTexture (VertexTexInfo info, Bitmap data) + { + this.originals [info] = new TextureInfo {Data = data, Texinfo = info}; + } + + /// + /// The meat of the class, takes all the UV information and builds an image with all the required information + /// + public Bitmap BuildAtlas (out int width, out int height, out Dictionary topleft) + { + topleft = new Dictionary (); + width = 1024 * 4; + height = 128; + int currentX = 0; + + // width of the atlas will be a multiple of 1024 to fit multiple 1024 textures horizontally + foreach (KeyValuePair pair in originals) + { + Vector2 textureSize = TextureUtils.TextureSize (pair.Key.Color); + topleft.Add (pair.Value, new Vector2 (currentX, height - 128)); + + int nextX = currentX + (int) textureSize.X; + + if (nextX >= width) + { + currentX = 0; + height += 128; + } + else + { + currentX = nextX; + } + } + + // the last texture added filled a row, do not create an empty row + if (currentX == 0) + { + height -= 128; + } + + Bitmap result = new Bitmap (width, height); + + currentX = 0; + int currentY = 0; + + // now apply the same algorithm but copying the data out + foreach (KeyValuePair pair in originals) + { + Vector2 textureSize = TextureUtils.TextureSize (pair.Key.Color); + + // copy pixels manually + for (int x = 0; x < textureSize.X; x++) + { + for (int y = 0; y < textureSize.Y; y++) + { + result.SetPixel (x + currentX, y + currentY, pair.Value.Data.GetPixel (x, y)); + } + } + + // determine next point to get pixels from + int nextX = currentX + (int) textureSize.X; + + if (nextX >= width) + { + currentX = 0; + currentY += 128; + } + else + { + currentX = nextX; + } + } + + return result; + } +} \ No newline at end of file diff --git a/CrashEdit/Exporters/TextureExporter.cs b/CrashEdit/Exporters/TextureExporter.cs new file mode 100644 index 00000000..6ac49be4 --- /dev/null +++ b/CrashEdit/Exporters/TextureExporter.cs @@ -0,0 +1,175 @@ +using System.Drawing; +using System.Drawing.Imaging; +using CrashEdit.CE; + +namespace CrashEdit.Exporters; + +public class TextureExporter +{ + public static Bitmap Create4bpp (byte [] data, VertexTexInfo 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, VertexTexInfo 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, VertexTexInfo 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, VertexTexInfo 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 82588a9f..6fba6437 100755 --- a/CrashEdit/FileUtil.cs +++ b/CrashEdit/FileUtil.cs @@ -46,6 +46,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) { ArgumentNullException.ThrowIfNull(data); diff --git a/CrashEdit/Utils/OpenTK Integration/AnimationRenderer.cs b/CrashEdit/Utils/OpenTK Integration/AnimationRenderer.cs index 061610bc..26f0d02a 100644 --- a/CrashEdit/Utils/OpenTK Integration/AnimationRenderer.cs +++ b/CrashEdit/Utils/OpenTK Integration/AnimationRenderer.cs @@ -147,7 +147,7 @@ private bool RenderFrame(VAO[] vaos, Frame frame, int buf) } foreach (var tri in model.Triangles) { - if (!ProcessTextureInfoC2(Render.RealCurrentFrame / 2, tri.Texture, tri.Animated, model.Textures, model.AnimatedTextures, out var polygon_texture_info)) + if (!TextureUtils.ProcessTextureInfoC2(Render.RealCurrentFrame / 2, tri.Texture, tri.Animated, model.Textures, model.AnimatedTextures, out var polygon_texture_info)) continue; bool nocull = tri.Subtype == 0 || tri.Subtype == 2; bool flip = (tri.Type == 2 ^ tri.Subtype == 3) && !nocull; diff --git a/CrashEdit/Utils/TextureUtils.cs b/CrashEdit/Utils/TextureUtils.cs new file mode 100644 index 00000000..0306989d --- /dev/null +++ b/CrashEdit/Utils/TextureUtils.cs @@ -0,0 +1,74 @@ +using CrashEdit.Crash; +using OpenTK.Mathematics; + +namespace CrashEdit; + +public class TextureUtils +{ + public static bool ProcessTextureInfoC2(long texture_frame, int in_tex_id, bool animated, IList textures, IList animated_textures, out ModelTexture tex) + { + if (in_tex_id != 0 || animated) + { + int tex_id = in_tex_id - 1; + if (animated) + { + if (++tex_id >= animated_textures.Count) + { + tex = default; + return false; + } + 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)((texture_frame / (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) + { + tex = default; + return false; + } + tex = textures[tex_id]; + } + else + { + tex = default; + } + } + else + { + if (tex_id >= textures.Count) + { + tex = default; + return false; + } + tex = textures[tex_id]; + } + return true; + } + tex = default; + return true; + } + + public static Vector2 TextureSize (int colorMode) + { + return colorMode switch + { + 0 => new Vector2 (1024, 128), + 1 => new Vector2 (512, 128), + _ => new Vector2 (256, 128) + }; + } +} \ No newline at end of file