ZoneBuilder/Source/Plugins/BuilderEffects/Modes/ImportObjAsTerrainMode.cs

407 lines
11 KiB
C#
Raw Normal View History

#region ================== Namespaces
using System;
using System.Collections.Generic;
using CodeImp.DoomBuilder.Editing;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Geometry;
using System.IO;
using System.Globalization;
using CodeImp.DoomBuilder.Windows;
using CodeImp.DoomBuilder.Map;
#endregion
namespace CodeImp.DoomBuilder.BuilderEffects
{
[EditMode(DisplayName = "Terrain Importer",
SwitchAction = "importobjasterrain",
Volatile = true,
UseByDefault = true,
AllowCopyPaste = false)]
public class ImportObjAsTerrainMode : ClassicMode
{
#region ================== Constants
private readonly static char[] space = { ' ' };
private const string slash = "/";
internal const int VERTEX_HEIGHT_THING_TYPE = 1504;
#endregion
#region ================== Variables
private struct Face
{
public readonly Vector3D V1;
public readonly Vector3D V2;
public readonly Vector3D V3;
public Face(Vector3D v1, Vector3D v2, Vector3D v3)
{
V1 = v1;
V2 = v2;
V3 = v3;
}
}
private readonly ObjImportSettingsForm form;
#endregion
#region ================== Properties
internal enum UpAxis
{
Y,
Z,
X
}
#endregion
#region ================== Constructor
public ImportObjAsTerrainMode()
{
form = new ObjImportSettingsForm();
}
#endregion
#region ================== Methods
public override void OnEngage()
{
base.OnEngage();
General.Map.Map.ClearAllSelected();
//show interface
if(form.ShowDialog() == DialogResult.OK && File.Exists(form.FilePath))
{
OnAccept();
}
else
{
OnCancel();
}
}
public override void OnAccept()
{
Cursor.Current = Cursors.AppStarting;
General.Interface.DisplayStatus(StatusType.Busy, "Creating geometry...");
// Collections! Everyone loves them!
List<Vector3D> verts = new List<Vector3D>(12);
List<Face> faces = new List<Face>(4);
int minZ = int.MaxValue;
int maxZ = int.MinValue;
// Read .obj, create and select sectors
if(!ReadGeometry(form.FilePath, form.ObjScale, form.UpAxis, verts, faces, ref minZ, ref maxZ) || !CreateGeometry(verts, faces, maxZ + (maxZ - minZ)/2))
{
// Fial!
Cursor.Current = Cursors.Default;
// Return to base mode
General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name);
return;
}
// Update caches
General.Map.Map.SnapAllToAccuracy();
General.Map.Map.UpdateConfiguration();
General.Map.Map.Update();
General.Map.IsChanged = true;
// Update things filter so that it includes added things
if(form.UseVertexHeights && !General.Map.UDMF) General.Map.ThingsFilter.Update();
// Done
Cursor.Current = Cursors.Default;
// Switch to Edit Selection mode
General.Editing.ChangeMode("EditSelectionMode", true);
}
public override void OnCancel()
{
// Cancel base class
base.OnCancel();
// Return to base mode
General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name);
}
#endregion
#region ================== Geometry creation
private bool CreateGeometry(List<Vector3D> verts, List<Face> faces, int maxZ)
{
if(verts.Count < 3 || faces.Count == 0)
{
MessageBox.Show("Cannot import the model: failed to find any suitable polygons!",
"Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
MapSet map = General.Map.Map;
// Capacity checks
int totalverts = map.Vertices.Count + verts.Count;
int totalsides = map.Sidedefs.Count + faces.Count * 6;
int totallines = map.Linedefs.Count + faces.Count * 3;
int totalsectors = map.Sectors.Count + faces.Count;
int totalthings = 0;
if(form.UseVertexHeights && !General.Map.UDMF)
{
// We'll use vertex height things in non-udmf maps, if such things are defined in Game Configuration
totalthings = map.Things.Count + verts.Count;
}
if(totalverts > General.Map.FormatInterface.MaxVertices)
{
MessageBox.Show("Cannot import the model: resulting vertex count (" + totalverts
+ ") is larger than map format's maximum (" + General.Map.FormatInterface.MaxVertices + ")!",
"Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if(totalsides > General.Map.FormatInterface.MaxSidedefs)
{
MessageBox.Show("Cannot import the model: resulting sidedefs count (" + totalsides
+ ") is larger than map format's maximum (" + General.Map.FormatInterface.MaxSidedefs + ")!",
"Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if(totallines > General.Map.FormatInterface.MaxLinedefs)
{
MessageBox.Show("Cannot import the model: resulting sidedefs count (" + totallines
+ ") is larger than map format's maximum (" + General.Map.FormatInterface.MaxLinedefs + ")!",
"Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if(totalsectors > General.Map.FormatInterface.MaxSectors)
{
MessageBox.Show("Cannot import the model: resulting sidedefs count (" + totalsectors
+ ") is larger than map format's maximum (" + General.Map.FormatInterface.MaxSectors + ")!",
"Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if(totalthings > General.Map.FormatInterface.MaxThings)
{
MessageBox.Show("Cannot import the model: resulting things count (" + totalthings
+ ") is larger than map format's maximum (" + General.Map.FormatInterface.MaxThings + ")!",
"Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
//make undo
General.Map.UndoRedo.CreateUndo("Import Terrain");
//prepare mapset
List<Linedef> newlines = new List<Linedef>();
map.BeginAddRemove();
map.SetCapacity(totalverts, totallines, totalsides, totalsectors, totalthings);
//terrain has many faces... let's create them
Dictionary<Vector3D, Vertex> newverts = new Dictionary<Vector3D, Vertex>();
foreach(Face face in faces)
{
// Create sector
Sector s = map.CreateSector();
s.Selected = true;
s.FloorHeight = (int)Math.Round((face.V1.z + face.V2.z + face.V3.z) / 3);
s.CeilHeight = maxZ;
s.Brightness = General.Settings.DefaultBrightness; //todo: allow user to change this
s.SetCeilTexture(General.Map.Config.SkyFlatName);
s.SetFloorTexture(General.Map.Options.DefaultFloorTexture); //todo: allow user to change this
// And linedefs
Linedef newline = GetLine(newverts, s, face.V1, face.V2);
if(newline != null) newlines.Add(newline);
newline = GetLine(newverts, s, face.V2, face.V3);
if(newline != null) newlines.Add(newline);
newline = GetLine(newverts, s, face.V3, face.V1);
if(newline != null) newlines.Add(newline);
s.UpdateCache();
}
// Add slope things
if(form.UseVertexHeights && !General.Map.UDMF)
{
foreach(Vector3D pos in newverts.Keys)
{
Thing t = map.CreateThing();
General.Settings.ApplyDefaultThingSettings(t);
t.SRB2Type = VERTEX_HEIGHT_THING_TYPE;
t.Move(pos);
t.Selected = true;
}
}
//update new lines
foreach(Linedef l in newlines) l.ApplySidedFlags();
map.EndAddRemove();
return true;
}
private Linedef GetLine(Dictionary<Vector3D, Vertex> verts, Sector sector, Vector3D v1, Vector3D v2)
{
Linedef line = null;
//get start and end verts
Vertex start = GetVertex(verts, v1);
Vertex end = GetVertex(verts, v2);
//check if the line is already created
foreach(Linedef l in start.Linedefs)
{
if(l.End == end || l.Start == end)
{
line = l;
break;
}
}
//create a new line?
Sidedef side;
if(line == null)
{
line = General.Map.Map.CreateLinedef(start, end);
//create front sidedef and attach sector to it
side = General.Map.Map.CreateSidedef(line, true, sector);
}
else
{
//create back sidedef and attach sector to it
side = General.Map.Map.CreateSidedef(line, false, sector);
}
if(!form.UseVertexHeights) side.SetTextureLow(General.Map.Options.DefaultWallTexture);
line.Selected = true;
return line;
}
private Vertex GetVertex(Dictionary<Vector3D, Vertex> verts, Vector3D pos)
{
//already there?
if(verts.ContainsKey(pos)) return verts[pos];
//make a new one
Vertex v = General.Map.Map.CreateVertex(pos);
if(form.UseVertexHeights) v.ZFloor = pos.z;
verts.Add(pos, v);
return v;
}
#endregion
#region ================== .obj import
private static bool ReadGeometry(string path, float scale, UpAxis axis, List<Vector3D> verts, List<Face> faces, ref int minZ, ref int maxZ)
{
using(StreamReader reader = File.OpenText(path))
{
string line;
int counter = 0;
float picoarse = (float)Math.Round(Angle2D.PI, 3);
while((line = reader.ReadLine()) != null)
{
counter++;
if(line.StartsWith("v "))
{
string[] parts = line.Split(space, StringSplitOptions.RemoveEmptyEntries);
float x, y, z;
if(parts.Length != 4 || !float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out x) ||
!float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out y) ||
!float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out z))
{
MessageBox.Show("Failed to parse vertex definition at line " + counter + "!", "Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
//apply up axis
int px, py, pz;
switch(axis)
{
case UpAxis.Y:
px = (int)Math.Round(x * scale);
py = (int)Math.Round(-z * scale);
pz = (int)Math.Round(y * scale);
break;
case UpAxis.Z:
px = (int)Math.Round(-x * scale);
py = (int)Math.Round(-y * scale);
pz = (int)Math.Round(z * scale);
break;
case UpAxis.X:
px = (int)Math.Round(-y * scale);
py = (int)Math.Round(-z * scale);
pz = (int)Math.Round(x * scale);
break;
default: //same as UpAxis.Y
px = (int)Math.Round(x * scale);
py = (int)Math.Round(-z * scale);
pz = (int)Math.Round(y * scale);
break;
}
if(maxZ < pz) maxZ = pz;
if(minZ > pz) minZ = pz;
verts.Add(new Vector3D(px, py, pz));
}
else if(line.StartsWith("f "))
{
string[] parts = line.Split(space, StringSplitOptions.RemoveEmptyEntries);
if(parts.Length != 4)
{
MessageBox.Show("Failed to parse face definition at line " + counter + ": only triangular faces are supported!", "Terrain Importer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
//.obj vertex indices are 1-based
int v1 = ReadVertexIndex(parts[1]) - 1;
int v2 = ReadVertexIndex(parts[2]) - 1;
int v3 = ReadVertexIndex(parts[3]) - 1;
// Skip face if vertex indexes match
if(verts[v1] == verts[v2] || verts[v1] == verts[v3] || verts[v2] == verts[v3]) continue;
// Skip face if it's vertical
Plane p = new Plane(verts[v1], verts[v2], verts[v3], true);
if((float)Math.Round(p.Normal.GetAngleZ(), 3) == picoarse) continue;
faces.Add(new Face(verts[v3], verts[v2], verts[v1]));
}
}
}
return true;
}
private static int ReadVertexIndex(string def)
{
int slashpos = def.IndexOf(slash);
if(slashpos != -1) def = def.Substring(0, slashpos);
return int.Parse(def);
}
#endregion
}
}