mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-11-26 13:51:40 +00:00
838 lines
27 KiB
C#
Executable file
838 lines
27 KiB
C#
Executable file
#region ================== Namespaces
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using CodeImp.DoomBuilder.Data;
|
|
using CodeImp.DoomBuilder.Geometry;
|
|
using CodeImp.DoomBuilder.Map;
|
|
using CodeImp.DoomBuilder.Rendering;
|
|
using CodeImp.DoomBuilder.VisualModes;
|
|
using CodeImp.DoomBuilder.Windows;
|
|
using CodeImp.DoomBuilder.BuilderModes.Interface;
|
|
using System.Windows.Forms;
|
|
using System.Linq;
|
|
|
|
#endregion
|
|
|
|
namespace CodeImp.DoomBuilder.BuilderModes.IO
|
|
{
|
|
#region ================== Structs
|
|
|
|
internal struct WavefrontExportSettings
|
|
{
|
|
public string Obj;
|
|
public readonly string ObjName;
|
|
public readonly string ObjPath;
|
|
public readonly float Scale;
|
|
public readonly bool ExportForGZDoom;
|
|
public readonly bool ExportTextures;
|
|
public bool Valid;
|
|
public string[] Textures;
|
|
public string[] Flats;
|
|
public string ActorName;
|
|
public string BasePath;
|
|
public string ActorPath;
|
|
public string ModelPath;
|
|
public List<string> SkipTextures;
|
|
public bool IgnoreControlSectors;
|
|
public bool NormalizeLowestVertex;
|
|
public bool CenterModel;
|
|
public bool ZScript;
|
|
public bool GenerateCode;
|
|
public bool GenerateModeldef;
|
|
|
|
// Actor properties and flags
|
|
public int Radius;
|
|
public int Height;
|
|
public string Sprite;
|
|
public bool NoGravity;
|
|
public bool SpawnOnCeiling;
|
|
public bool Solid;
|
|
|
|
public WavefrontExportSettings(WavefrontSettingsForm form)
|
|
{
|
|
ObjName = Path.GetFileNameWithoutExtension(form.FilePath);
|
|
ObjPath = Path.GetDirectoryName(form.FilePath);
|
|
Scale = form.ObjScale;
|
|
ExportForGZDoom = form.UseGZDoomScale;
|
|
ExportTextures = form.ExportTextures;
|
|
|
|
ActorName = form.ActorName.Trim();
|
|
BasePath = form.BasePath;
|
|
ActorPath = form.ActorPath;
|
|
ModelPath = form.ModelPath;
|
|
SkipTextures = form.SkipTextures;
|
|
IgnoreControlSectors = form.IgnoreControlSectors;
|
|
NormalizeLowestVertex = form.NormalizeLowestVertex;
|
|
CenterModel = form.CenterModel;
|
|
ZScript = form.ZScript;
|
|
GenerateCode = form.GenerateCode;
|
|
GenerateModeldef = form.GenerateModeldef;
|
|
|
|
NoGravity = form.NoGravity;
|
|
SpawnOnCeiling = form.SpawnOnCeiling;
|
|
Solid = form.Solid;
|
|
Sprite = form.Sprite;
|
|
|
|
Radius = 20;
|
|
Height = 16;
|
|
|
|
Valid = false;
|
|
Obj = string.Empty;
|
|
Textures = null;
|
|
Flats = null;
|
|
|
|
if (ExportForGZDoom)
|
|
SkipTextures.Add("-");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
internal class WavefrontExporter
|
|
{
|
|
|
|
#region ================== Variables and structs
|
|
|
|
private const string DEFAULT = "Default";
|
|
|
|
private struct VertexIndices
|
|
{
|
|
public int PositionIndex;
|
|
public int UVIndex;
|
|
public int NormalIndex;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Export
|
|
|
|
public void Export(ICollection<Sector> sectors, WavefrontExportSettings settings)
|
|
{
|
|
CreateObjFromSelection(sectors, ref settings);
|
|
|
|
if(!settings.Valid)
|
|
{
|
|
General.Interface.DisplayStatus(StatusType.Warning, "OBJ creation failed. Check 'Errors and Warnings' window for details.");
|
|
return;
|
|
}
|
|
|
|
// Export Textures, but only of it's not exporting for GZDoom
|
|
if(settings.ExportTextures && !settings.ExportForGZDoom)
|
|
{
|
|
//save all used textures
|
|
if(settings.Textures != null)
|
|
{
|
|
foreach(string s in settings.Textures)
|
|
{
|
|
if(s == DEFAULT) continue;
|
|
if(General.Map.Data.GetTextureExists(s))
|
|
{
|
|
ImageData id = General.Map.Data.GetTextureImage(s);
|
|
if(id.Width == 0 || id.Height == 0)
|
|
{
|
|
General.ErrorLogger.Add(ErrorType.Warning, "OBJ Exporter: texture \"" + s + "\" has invalid size (" + id.Width + "x" + id.Height + ")!");
|
|
continue;
|
|
}
|
|
|
|
Bitmap bmp = id.ExportBitmap();
|
|
lock (bmp)
|
|
{
|
|
string filepath = Path.Combine(settings.ObjPath, Path.GetDirectoryName(s), Path.GetFileNameWithoutExtension(s) + ".png");
|
|
|
|
// Make sure the directory is there
|
|
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
|
|
|
|
bmp.Save(filepath, ImageFormat.Png);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
General.ErrorLogger.Add(ErrorType.Warning, "OBJ Exporter: texture \"" + s + "\" does not exist!");
|
|
}
|
|
}
|
|
}
|
|
|
|
if(settings.Flats != null)
|
|
{
|
|
foreach(string s in settings.Flats)
|
|
{
|
|
if(s == DEFAULT) continue;
|
|
if(General.Map.Data.GetFlatExists(s))
|
|
{
|
|
ImageData id = General.Map.Data.GetFlatImage(s);
|
|
if(id.Width == 0 || id.Height == 0)
|
|
{
|
|
General.ErrorLogger.Add(ErrorType.Warning, "OBJ Exporter: flat \"" + s + "\" has invalid size (" + id.Width + "x" + id.Height + ")!");
|
|
continue;
|
|
}
|
|
|
|
Bitmap bmp = id.ExportBitmap();
|
|
|
|
// Handle duplicate names
|
|
string flatname = s;
|
|
if(settings.Textures != null && Array.IndexOf(settings.Textures, s) != -1)
|
|
flatname += "_FLAT";
|
|
|
|
lock (bmp)
|
|
{
|
|
bmp.Save(Path.Combine(settings.ObjPath, Path.GetFileNameWithoutExtension(flatname) + ".PNG"), ImageFormat.Png);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
General.ErrorLogger.Add(ErrorType.Warning, "OBJ Exporter: flat \"" + s + "\" does not exist!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//write obj
|
|
string savePath;
|
|
|
|
if (settings.ExportForGZDoom)
|
|
savePath = Path.Combine(settings.ModelPath, settings.ActorName + ".obj");
|
|
else
|
|
savePath = Path.Combine(settings.ObjPath, settings.ObjName + ".obj");
|
|
|
|
// Make sure the directory is there
|
|
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
|
|
|
|
using (StreamWriter sw = new StreamWriter(savePath, false))
|
|
sw.Write(settings.Obj);
|
|
|
|
//create mtl
|
|
StringBuilder mtl = new StringBuilder();
|
|
mtl.Append("# MTL for " + General.Map.FileTitle + ", map " + General.Map.Options.LevelName + Environment.NewLine);
|
|
mtl.Append("# Created by Ultimate Doom Builder " + Application.ProductVersion + Environment.NewLine + Environment.NewLine);
|
|
|
|
if(settings.Textures != null)
|
|
{
|
|
foreach(string s in settings.Textures)
|
|
{
|
|
if(s == DEFAULT) continue;
|
|
|
|
string filepath = Path.Combine(settings.ObjPath, Path.GetDirectoryName(s), Path.GetFileNameWithoutExtension(s) + ".png");
|
|
|
|
mtl.Append("newmtl " + s + Environment.NewLine);
|
|
mtl.Append("Kd 1.0 1.0 1.0" + Environment.NewLine);
|
|
if(settings.ExportTextures) mtl.Append("map_Kd " + filepath + Environment.NewLine);
|
|
mtl.Append(Environment.NewLine);
|
|
}
|
|
}
|
|
|
|
if(settings.Flats != null)
|
|
{
|
|
foreach(string s in settings.Flats)
|
|
{
|
|
if(s == DEFAULT) continue;
|
|
mtl.Append("newmtl " + s + Environment.NewLine);
|
|
mtl.Append("Kd 1.0 1.0 1.0" + Environment.NewLine);
|
|
if(settings.ExportTextures)
|
|
{
|
|
// Handle duplicate names
|
|
string flatsuffix = string.Empty;
|
|
if(settings.Textures != null && Array.IndexOf(settings.Textures, s) != -1)
|
|
flatsuffix = "_FLAT";
|
|
|
|
string filepath = Path.Combine(settings.ObjPath, Path.GetDirectoryName(s), Path.GetFileNameWithoutExtension(s) + flatsuffix + ".png");
|
|
|
|
mtl.Append("map_Kd " + Path.Combine(settings.ObjPath, filepath) + Environment.NewLine);
|
|
}
|
|
mtl.Append(Environment.NewLine);
|
|
}
|
|
}
|
|
|
|
if (!settings.ExportForGZDoom)
|
|
{
|
|
// Make sure the directory is there
|
|
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
|
|
|
|
string mtlPath = Path.Combine(Path.GetDirectoryName(savePath), Path.GetFileNameWithoutExtension(savePath) + ".mtl");
|
|
|
|
// Write mtl (only if not exporting for GZDoom, since it will be ignored anyway
|
|
using (StreamWriter sw = new StreamWriter(mtlPath, false))
|
|
sw.Write(mtl.ToString());
|
|
}
|
|
else
|
|
{
|
|
// Create ZScript or DECORATE
|
|
if (settings.GenerateCode)
|
|
{
|
|
Stream stream;
|
|
string path = Path.Combine(settings.ActorPath, settings.ActorName);
|
|
|
|
if (settings.ZScript)
|
|
{
|
|
stream = BuilderPlug.Me.GetResourceStream("ObjExportZScriptTemplate.txt");
|
|
path += ".zs";
|
|
}
|
|
else
|
|
{
|
|
stream = BuilderPlug.Me.GetResourceStream("ObjExportDecorateTemplate.txt");
|
|
path += ".txt";
|
|
}
|
|
|
|
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
|
|
{
|
|
string template = reader.ReadToEnd();
|
|
|
|
template = template.Replace("{ActorName}", settings.ActorName);
|
|
template = template.Replace("{Sprite}", settings.Sprite);
|
|
template = template.Replace("{FlagNoGravity}", settings.NoGravity ? "+NOGRAVITY" : "");
|
|
template = template.Replace("{FlagSpawnOnCeiling}", settings.SpawnOnCeiling ? "+SPAWNCEILING" : "");
|
|
template = template.Replace("{FlagSolid}", settings.Solid ? "+SOLID" : "");
|
|
template = template.Replace("{FlagInvulnerable}", settings.Solid ? "+INVULNERABLE" : "");
|
|
template = template.Replace("{FlagNoDamage}", settings.Solid ? "+NODAMAGE" : "");
|
|
template = template.Replace("{FlagShootable}", settings.Solid ? "+SHOOTABLE" : "");
|
|
template = template.Replace("{FlagNotAutoAimed}", settings.Solid ? "+NOTAUTOAIMED" : "");
|
|
template = template.Replace("{FlagNeverTarget}", settings.Solid ? "+NEVERTARGET" : "");
|
|
template = template.Replace("{FlagDontThrust}", settings.Solid ? "+DONTTHRUST" : "");
|
|
template = template.Replace("{PropRadius}", settings.Radius.ToString());
|
|
template = template.Replace("{PropHeight}", settings.Height.ToString());
|
|
|
|
// Make sure the directory is there
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
|
|
using (StreamWriter sw = new StreamWriter(path, false))
|
|
sw.Write(template);
|
|
}
|
|
}
|
|
|
|
// Create MODELDEF
|
|
if (settings.GenerateModeldef)
|
|
{
|
|
Stream stream = BuilderPlug.Me.GetResourceStream("ObjExportModeldefTemplate.txt");
|
|
|
|
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
|
|
{
|
|
string path = Path.Combine(settings.BasePath, "modeldef." + settings.ActorName + ".txt");
|
|
string template = reader.ReadToEnd();
|
|
|
|
// The path to the model is relative to the base path, so generate the base path
|
|
string basepath = settings.BasePath.Trim();
|
|
string modelpath = settings.ModelPath.Trim();
|
|
|
|
// Make sue there's a directory separator at the end of the paths, otherwise it'll not work correctly
|
|
if (!basepath.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
|
basepath += Path.DirectorySeparatorChar;
|
|
|
|
if (!modelpath.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
|
modelpath += Path.DirectorySeparatorChar;
|
|
|
|
Uri baseUri = new Uri(basepath);
|
|
Uri modelUri = new Uri(modelpath);
|
|
|
|
Uri relativeUri = baseUri.MakeRelativeUri(modelUri);
|
|
string relativepath = Uri.UnescapeDataString(relativeUri.OriginalString);
|
|
|
|
template = template.Replace("{ActorName}", settings.ActorName);
|
|
template = template.Replace("{ModelPath}", relativepath);
|
|
template = template.Replace("{Sprite}", settings.Sprite);
|
|
|
|
// Make sure the directory is there
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
|
|
using (StreamWriter sw = new StreamWriter(path, false))
|
|
sw.Write(template);
|
|
}
|
|
}
|
|
}
|
|
|
|
//done
|
|
General.Interface.DisplayStatus(StatusType.Info, "Geometry exported to \"" + savePath);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Utility
|
|
|
|
private static void CreateObjFromSelection(ICollection<Sector> sectors, ref WavefrontExportSettings data)
|
|
{
|
|
BaseVisualMode mode = new BaseVisualMode();
|
|
bool renderingEffectsDisabled = false;
|
|
|
|
if(!General.Settings.EnhancedRenderingEffects)
|
|
{
|
|
renderingEffectsDisabled = true;
|
|
mode.ToggleEnhancedRendering();
|
|
}
|
|
|
|
mode.RebuildElementData();
|
|
|
|
List<BaseVisualSector> visualSectors = new List<BaseVisualSector>();
|
|
|
|
//create visual geometry
|
|
foreach(Sector s in sectors)
|
|
{
|
|
bool addvs = true;
|
|
|
|
// Check if the sector has, or shares a line with a 3D floor control sector, and ignore it if necessary
|
|
if (data.ExportForGZDoom && data.IgnoreControlSectors)
|
|
{
|
|
foreach (Sidedef sd in s.Sidedefs)
|
|
{
|
|
if (sd.Line.Action >= 100 && sd.Line.Action < 300)
|
|
{
|
|
addvs = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (addvs)
|
|
{
|
|
BaseVisualSector bvs = mode.CreateBaseVisualSector(s);
|
|
|
|
if (bvs != null)
|
|
{
|
|
// When the visual sector is created yet unseen wall textures are not fully loaded in time, which can result in the
|
|
// texture coordinates (UV) ending up as NaN. This can happen if the export is started before the textures are loaded
|
|
// (for example by opening the texture browser or going into visual mode. So make sure the textures are loaded immediately.
|
|
// After that the visual sector has to be rebuilt. This only seems to affect wall textures, not floors/ceilings
|
|
// See https://github.com/UltimateDoomBuilder/UltimateDoomBuilder/issues/1015
|
|
foreach (VisualSidedefParts vsp in bvs.Sides.Values)
|
|
{
|
|
if (vsp.upper?.Texture != null && vsp.upper.Texture.ImageState != ImageLoadState.Ready)
|
|
vsp.upper.Texture.LocalGetBitmap(true);
|
|
|
|
if (vsp.middlesingle?.Texture != null && vsp.middlesingle.Texture.ImageState != ImageLoadState.Ready)
|
|
vsp.middlesingle.Texture.LocalGetBitmap(true);
|
|
|
|
if (vsp.middledouble?.Texture != null && vsp.middledouble.Texture.ImageState != ImageLoadState.Ready)
|
|
vsp.middledouble.Texture.LocalGetBitmap(true);
|
|
|
|
if (vsp.lower?.Texture != null && vsp.lower.Texture.ImageState != ImageLoadState.Ready)
|
|
vsp.lower.Texture.LocalGetBitmap(true);
|
|
|
|
if (vsp.middle3d != null)
|
|
{
|
|
foreach (VisualMiddle3D vm in vsp.middle3d.Where(o => o.Texture != null && o.Texture.ImageState != ImageLoadState.Ready))
|
|
{
|
|
vm.Texture.LocalGetBitmap(true);
|
|
}
|
|
}
|
|
|
|
if (vsp.middleback != null)
|
|
{
|
|
foreach (VisualMiddleBack vm in vsp.middleback.Where(o => o.Texture != null && o.Texture.ImageState != ImageLoadState.Ready))
|
|
{
|
|
vm.Texture.LocalGetBitmap(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rebuild the visual sector so that the texture coordinates are guaranteed to be correct
|
|
bvs.Rebuild();
|
|
|
|
visualSectors.Add(bvs);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (visualSectors.Count == 0)
|
|
{
|
|
General.ErrorLogger.Add(ErrorType.Error, "OBJ Exporter: no visual sectors to export!");
|
|
return;
|
|
}
|
|
|
|
//sort geometry
|
|
List<Dictionary<string, List<WorldVertex[]>>> geometryByTexture = SortGeometry(visualSectors, data.SkipTextures, !data.ExportForGZDoom);
|
|
|
|
//restore vm settings
|
|
if(renderingEffectsDisabled) mode.ToggleEnhancedRendering();
|
|
mode.Dispose();
|
|
|
|
//create obj
|
|
StringBuilder obj = CreateObjGeometry(geometryByTexture, ref data);
|
|
|
|
if(obj.Length == 0)
|
|
{
|
|
General.ErrorLogger.Add(ErrorType.Error, "OBJ Exporter: failed to create geometry!");
|
|
return;
|
|
}
|
|
|
|
//add header
|
|
obj.Insert(0, "o " + General.Map.Options.LevelName + Environment.NewLine); //name
|
|
obj.Insert(0, "# Created by Ultimate Doom Builder " + Application.ProductVersion + Environment.NewLine + Environment.NewLine);
|
|
obj.Insert(0, "# " + General.Map.FileTitle + ", map " + General.Map.Options.LevelName + Environment.NewLine);
|
|
data.Obj = obj.ToString();
|
|
|
|
string[] textures = new string[geometryByTexture[0].Keys.Count];
|
|
geometryByTexture[0].Keys.CopyTo(textures, 0);
|
|
Array.Sort(textures);
|
|
data.Textures = textures;
|
|
|
|
string[] flats = new string[geometryByTexture[1].Keys.Count];
|
|
geometryByTexture[1].Keys.CopyTo(flats, 0);
|
|
Array.Sort(flats);
|
|
data.Flats = flats;
|
|
|
|
data.Valid = true;
|
|
}
|
|
|
|
private static List<Dictionary<string, List<WorldVertex[]>>> SortGeometry(List<BaseVisualSector> visualSectors, List<string>skipTextures, bool defaultOnMissingTextures)
|
|
{
|
|
var texturegeo = new Dictionary<string, List<WorldVertex[]>>(StringComparer.Ordinal);
|
|
var flatgeo = new Dictionary<string, List<WorldVertex[]>>(StringComparer.Ordinal);
|
|
|
|
if (defaultOnMissingTextures)
|
|
{
|
|
texturegeo.Add(DEFAULT, new List<WorldVertex[]>());
|
|
flatgeo.Add(DEFAULT, new List<WorldVertex[]>());
|
|
}
|
|
|
|
foreach (BaseVisualSector vs in visualSectors)
|
|
{
|
|
//floor
|
|
string texture;
|
|
if(vs.Floor != null)
|
|
{
|
|
texture = vs.Sector.FloorTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref flatgeo, ref texture);
|
|
flatgeo[texture].AddRange(OptimizeGeometry(vs.Floor.Vertices, vs.Floor.GeometryType, vs.Floor.Sector.Sector.Labels.Count > 1));
|
|
}
|
|
}
|
|
|
|
//ceiling
|
|
if(vs.Ceiling != null)
|
|
{
|
|
texture = vs.Sector.CeilTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref flatgeo, ref texture);
|
|
flatgeo[texture].AddRange(OptimizeGeometry(vs.Ceiling.Vertices, vs.Ceiling.GeometryType, vs.Ceiling.Sector.Sector.Labels.Count > 1));
|
|
}
|
|
}
|
|
|
|
//walls
|
|
if(vs.Sides != null)
|
|
{
|
|
foreach(VisualSidedefParts part in vs.Sides.Values)
|
|
{
|
|
//upper
|
|
if(part.upper != null && part.upper.Vertices != null)
|
|
{
|
|
texture = part.upper.Sidedef.HighTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref texturegeo, ref texture);
|
|
texturegeo[texture].AddRange(OptimizeGeometry(part.upper.Vertices, part.upper.GeometryType));
|
|
}
|
|
}
|
|
|
|
//middle single
|
|
if(part.middlesingle != null && part.middlesingle.Vertices != null)
|
|
{
|
|
texture = part.middlesingle.Sidedef.MiddleTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref texturegeo, ref texture);
|
|
texturegeo[texture].AddRange(OptimizeGeometry(part.middlesingle.Vertices, part.middlesingle.GeometryType));
|
|
}
|
|
}
|
|
|
|
//middle double
|
|
if(part.middledouble != null && part.middledouble.Vertices != null)
|
|
{
|
|
texture = part.middledouble.Sidedef.MiddleTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref texturegeo, ref texture);
|
|
texturegeo[texture].AddRange(OptimizeGeometry(part.middledouble.Vertices, part.middledouble.GeometryType));
|
|
}
|
|
}
|
|
|
|
//middle3d
|
|
if(part.middle3d != null && part.middle3d.Count > 0)
|
|
{
|
|
foreach(VisualMiddle3D m3d in part.middle3d)
|
|
{
|
|
if(m3d.Vertices == null) continue;
|
|
texture = m3d.GetTextureName();
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref texturegeo, ref texture);
|
|
// 3D floor sides are cut so that there are only triangles for the visible parts. Some of those might be
|
|
// triangles and not rectangles, so we can't optimize them into rectangles
|
|
texturegeo[texture].AddRange(OptimizeGeometry(m3d.Vertices, m3d.GeometryType, true));
|
|
}
|
|
}
|
|
}
|
|
|
|
//backsides(?)
|
|
|
|
//lower
|
|
if(part.lower != null && part.lower.Vertices != null)
|
|
{
|
|
texture = part.lower.Sidedef.LowTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref texturegeo, ref texture);
|
|
texturegeo[texture].AddRange(OptimizeGeometry(part.lower.Vertices, part.lower.GeometryType));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//3d ceilings
|
|
foreach(VisualCeiling vc in vs.ExtraCeilings)
|
|
{
|
|
texture = vc.GetControlSector().CeilTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref flatgeo, ref texture);
|
|
flatgeo[texture].AddRange(OptimizeGeometry(vc.Vertices, (vc.ExtraFloor.VavoomType ? vc.GeometryType : VisualGeometryType.FLOOR)));
|
|
}
|
|
}
|
|
|
|
//3d floors
|
|
foreach(VisualFloor vf in vs.ExtraFloors)
|
|
{
|
|
texture = vf.GetControlSector().FloorTexture;
|
|
if (!skipTextures.Contains(texture))
|
|
{
|
|
CheckTextureName(ref flatgeo, ref texture);
|
|
flatgeo[texture].AddRange(OptimizeGeometry(vf.Vertices, (vf.ExtraFloor.VavoomType ? vf.GeometryType : VisualGeometryType.CEILING)));
|
|
}
|
|
}
|
|
|
|
//backsides(?)
|
|
}
|
|
|
|
return new List<Dictionary<string, List<WorldVertex[]>>> { texturegeo, flatgeo };
|
|
}
|
|
|
|
private static void CheckTextureName(ref Dictionary<string, List<WorldVertex[]>> geo, ref string texture)
|
|
{
|
|
if(!string.IsNullOrEmpty(texture) && texture != "-")
|
|
{
|
|
if(!geo.ContainsKey(texture)) geo.Add(texture, new List<WorldVertex[]>());
|
|
}
|
|
else
|
|
{
|
|
texture = DEFAULT;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Surface optimization
|
|
|
|
private static List<WorldVertex[]> OptimizeGeometry(WorldVertex[] verts, VisualGeometryType geotype)
|
|
{
|
|
return OptimizeGeometry(verts, geotype, false);
|
|
}
|
|
|
|
private static List<WorldVertex[]> OptimizeGeometry(WorldVertex[] verts, VisualGeometryType geotype, bool skiprectoptimization)
|
|
{
|
|
List<WorldVertex[]> groups = new List<WorldVertex[]>();
|
|
|
|
// Only do optimizations for walls right now. Doing them blindly for floors/ceilings will cause problems with concave sectors with 4 corners
|
|
if(!skiprectoptimization && verts.Length == 6 && (geotype != VisualGeometryType.CEILING && geotype != VisualGeometryType.FLOOR)) //rectangular surface
|
|
{
|
|
verts = new[] { verts[5], verts[2], verts[1], verts[0] };
|
|
groups.Add(verts);
|
|
}
|
|
else
|
|
{
|
|
for(int i = 0; i < verts.Length; i += 3)
|
|
{
|
|
groups.Add(new[] { verts[i + 2], verts[i + 1], verts[i] });
|
|
}
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== OBJ Creation
|
|
|
|
private static StringBuilder CreateObjGeometry(List<Dictionary<string, List<WorldVertex[]>>> geometryByTexture, ref WavefrontExportSettings data)
|
|
{
|
|
StringBuilder obj = new StringBuilder();
|
|
Vector2D offset;
|
|
const string vertexFormatter = "{0} {2} {1}\n";
|
|
|
|
Dictionary<Vector3D, int> uniqueVerts = new Dictionary<Vector3D, int>();
|
|
Dictionary<Vector3D, int> uniqueNormals = new Dictionary<Vector3D, int>();
|
|
Dictionary<PointF, int> uniqueUVs = new Dictionary<PointF, int>();
|
|
|
|
var vertexDataByTexture = new Dictionary<string, Dictionary<WorldVertex, VertexIndices>>(StringComparer.Ordinal);
|
|
int pc = 0;
|
|
int nc = 0;
|
|
int uvc = 0;
|
|
|
|
Vector3D tl = new Vector3D(double.MaxValue, double.MinValue, double.MinValue);
|
|
Vector3D br = new Vector3D(double.MinValue, double.MaxValue, double.MaxValue);
|
|
|
|
//optimize geometry
|
|
foreach (Dictionary<string, List<WorldVertex[]>> dictionary in geometryByTexture)
|
|
{
|
|
foreach(KeyValuePair<string, List<WorldVertex[]>> group in dictionary)
|
|
{
|
|
Dictionary<WorldVertex, VertexIndices> vertsData = new Dictionary<WorldVertex, VertexIndices>();
|
|
foreach(WorldVertex[] verts in group.Value)
|
|
{
|
|
//vertex normals. biwa not sure why I need to invert the normal's y component, but it seems to be necessary
|
|
Vector3D n = new Vector3D(verts[0].nx, verts[0].ny, verts[0].nz).GetNormal();
|
|
n.y *= -1;
|
|
|
|
int ni;
|
|
if(uniqueNormals.ContainsKey(n))
|
|
{
|
|
ni = uniqueNormals[n];
|
|
}
|
|
else
|
|
{
|
|
uniqueNormals.Add(n, ++nc);
|
|
ni = nc;
|
|
}
|
|
|
|
foreach(WorldVertex v in verts)
|
|
{
|
|
if(vertsData.ContainsKey(v)) continue;
|
|
VertexIndices indices = new VertexIndices();
|
|
indices.NormalIndex = ni;
|
|
|
|
//vertex coords
|
|
Vector3D vc = new Vector3D(v.x, v.y, v.z);
|
|
if(uniqueVerts.ContainsKey(vc))
|
|
{
|
|
indices.PositionIndex = uniqueVerts[vc];
|
|
}
|
|
else
|
|
{
|
|
uniqueVerts.Add(vc, ++pc);
|
|
indices.PositionIndex = pc;
|
|
}
|
|
|
|
//uv
|
|
PointF uv = new PointF(v.u, v.v);
|
|
if(uniqueUVs.ContainsKey(uv))
|
|
{
|
|
indices.UVIndex = uniqueUVs[uv];
|
|
}
|
|
else
|
|
{
|
|
uniqueUVs.Add(uv, ++uvc);
|
|
indices.UVIndex = uvc;
|
|
}
|
|
|
|
vertsData.Add(v, indices);
|
|
}
|
|
}
|
|
|
|
if(vertsData.Count > 0)
|
|
{
|
|
if(vertexDataByTexture.ContainsKey(group.Key))
|
|
{
|
|
foreach(KeyValuePair<WorldVertex, VertexIndices> g in vertsData)
|
|
vertexDataByTexture[group.Key].Add(g.Key, g.Value);
|
|
}
|
|
else
|
|
{
|
|
vertexDataByTexture.Add(group.Key, vertsData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the dimensions of the model
|
|
foreach(Dictionary<WorldVertex, VertexIndices> vdata in vertexDataByTexture.Values)
|
|
{
|
|
foreach(WorldVertex wv in vdata.Keys)
|
|
{
|
|
if (wv.x < tl.x) tl.x = wv.x;
|
|
if (wv.x > br.x) br.x = wv.x;
|
|
if (wv.y > tl.y) tl.y = wv.y;
|
|
if (wv.y < br.y) br.y = wv.y;
|
|
if (wv.z > tl.z) tl.z = wv.z;
|
|
if (wv.z < br.z) br.z = wv.z;
|
|
}
|
|
}
|
|
|
|
data.Radius = br.x - tl.x > tl.y - br.y ? (int)(tl.y - br.y) / 2 : (int)(br.x - tl.x) / 2;
|
|
data.Height = (int)(tl.z - br.z);
|
|
|
|
if (data.CenterModel)
|
|
offset = new Vector2D(tl.x + (br.x - tl.x) / 2.0, tl.y + (br.y - tl.y) / 2.0);
|
|
else
|
|
offset = new Vector2D(0.0, 0.0);
|
|
|
|
//write geometry
|
|
//write vertices
|
|
if (data.ExportForGZDoom)
|
|
{
|
|
|
|
|
|
foreach (KeyValuePair<Vector3D, int> group in uniqueVerts)
|
|
{
|
|
double z = (group.Key.z - (data.NormalizeLowestVertex ? br.z : 0)) * data.Scale * 1.2f;
|
|
|
|
obj.Append(string.Format(CultureInfo.InvariantCulture, "v " + vertexFormatter, (group.Key.x - offset.x) * data.Scale, -(group.Key.y - offset.y) * data.Scale, z));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// biwa. Not sure why the x-axis is flipped here, since it will result in wrong normals when using the model directly in GZDoom. For this reason
|
|
// I disabled the flipping above
|
|
foreach (KeyValuePair<Vector3D, int> group in uniqueVerts)
|
|
{
|
|
double z = (group.Key.z - (data.NormalizeLowestVertex ? br.z : 0)) * data.Scale;
|
|
|
|
obj.Append(string.Format(CultureInfo.InvariantCulture, "v " + vertexFormatter, -(group.Key.x - offset.x) * data.Scale, (group.Key.y - offset.y) * data.Scale, z));
|
|
}
|
|
}
|
|
|
|
//write normals
|
|
foreach(KeyValuePair<Vector3D, int> group in uniqueNormals)
|
|
obj.Append(string.Format(CultureInfo.InvariantCulture, "vn " + vertexFormatter, group.Key.x, group.Key.y, group.Key.z));
|
|
|
|
//write UV coords
|
|
foreach(KeyValuePair<PointF, int> group in uniqueUVs)
|
|
obj.Append(string.Format(CultureInfo.InvariantCulture, "vt {0} {1}\n", group.Key.X, -group.Key.Y));
|
|
|
|
// GZDoom ignores the material lib, so don't add it if the model is for GZDoom
|
|
if (!data.ExportForGZDoom)
|
|
{
|
|
obj.Append("mtllib ").Append(data.ObjName + ".mtl").Append("\n");
|
|
}
|
|
|
|
//write materials and surface indices
|
|
foreach(Dictionary<string, List<WorldVertex[]>> dictionary in geometryByTexture)
|
|
{
|
|
foreach(KeyValuePair<string, List<WorldVertex[]>> group in dictionary)
|
|
{
|
|
//material
|
|
obj.Append("usemtl ").Append(group.Key).Append("\n");
|
|
|
|
foreach(WorldVertex[] verts in group.Value)
|
|
{
|
|
//surface indices
|
|
obj.Append("f");
|
|
foreach(WorldVertex v in verts)
|
|
{
|
|
VertexIndices vi = vertexDataByTexture[group.Key][v];
|
|
obj.Append(" " + vi.PositionIndex + "/" + vi.UVIndex + "/" + vi.NormalIndex);
|
|
}
|
|
obj.Append("\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|