804 lines
33 KiB
C#
804 lines
33 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using FlaxEngine;
|
|
using FlaxEngine.Assertions;
|
|
using Console = Game.Console;
|
|
|
|
namespace Game
|
|
{
|
|
public class BrushGeometryMesh
|
|
{
|
|
public List<uint> indices = new List<uint>();
|
|
public MaterialBase material;
|
|
public List<Vector3> normals = new List<Vector3>();
|
|
public List<Vector2> uvs = new List<Vector2>();
|
|
public List<Vector3> vertices = new List<Vector3>();
|
|
}
|
|
|
|
public class BrushGeometry
|
|
{
|
|
public MapBrush brush;
|
|
public Dictionary<string, MaterialBase> brushMaterials;
|
|
public BrushGeometryMesh[] meshes;
|
|
public Model model;
|
|
public Vector3 offset;
|
|
public Vector3[] vertices; // all vertices
|
|
}
|
|
|
|
[ExecuteInEditMode]
|
|
public class Q3MapImporter : Script
|
|
{
|
|
//private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q1.map";
|
|
//private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q3.map";
|
|
//private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_valve.map";
|
|
//private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\dm4.map";
|
|
|
|
public string mapPath = @"C:\dev\Goake\maps\aerowalk\aerowalk.map";
|
|
//private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\problematic.map";
|
|
|
|
public bool importLights = false;
|
|
|
|
private Model model;
|
|
private MaterialBase missingMaterial;
|
|
|
|
private static void QuickHull(Vector3[] points, out Vector3[] outVertices)
|
|
{
|
|
var verts = new List<Vector3>();
|
|
var tris = new List<int>();
|
|
var normals = new List<Vector3>();
|
|
|
|
ConvexHullCalculator calc = new ConvexHullCalculator();
|
|
calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals);
|
|
|
|
var finalPoints = new List<Vector3>();
|
|
|
|
foreach (int tri in tris)
|
|
finalPoints.Add(verts[tri]);
|
|
|
|
outVertices = finalPoints.ToArray();
|
|
|
|
//verts = new QuickHull().QuickHull2(points);
|
|
//outVertices = verts.ToArray();
|
|
}
|
|
|
|
private MapEntity root;
|
|
|
|
private static IEnumerable<IEnumerable<T>> DifferentCombinations<T>(IEnumerable<T> elements, int k)
|
|
{
|
|
return k == 0
|
|
? new[] { new T[0] }
|
|
: elements.SelectMany((e, i) =>
|
|
DifferentCombinations(elements.Skip(i + 1), k - 1).Select(c => new[] { e }.Concat(c)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Triangulates the brush by calculating intersection points between triplets of planes.
|
|
/// Does not work well with off-axis aligned planes.
|
|
/// </summary>
|
|
public static void TriangulateBrush(MapBrush brush, out Vector3[] vertices)
|
|
{
|
|
var planePoints = new HashSet<Vector3>();
|
|
|
|
var planes = new List<Plane>();
|
|
float maxDist = 0f;
|
|
foreach (MapFacePlane brushPlane in brush.planes)
|
|
{
|
|
if (Mathf.Abs(brushPlane.plane.D) > maxDist)
|
|
maxDist = Mathf.Abs(brushPlane.plane.D);
|
|
planes.Add(brushPlane.plane);
|
|
}
|
|
|
|
maxDist *= Mathf.Sqrt(3);
|
|
|
|
var combinations = DifferentCombinations(planes, 3).ToList();
|
|
|
|
// pass 1: get all intersection points
|
|
foreach (var comb in combinations)
|
|
{
|
|
Plane p1 = comb.Skip(0).First();
|
|
Plane p2 = comb.Skip(1).First();
|
|
Plane p3 = comb.Skip(2).First();
|
|
|
|
//var maxDist = Math.Abs(p1.D * p2.D * p3.D);//Math.Max(p1.D, Math.Max(p2.D, p3.D));
|
|
|
|
// intersection of three planes
|
|
double denom = Vector3.Dot(p1.Normal, Vector3.Cross(p2.Normal, p3.Normal));
|
|
//if (denom < 0.0000000001)
|
|
// continue;
|
|
|
|
|
|
Vector3 intersection = (Vector3.Cross(p2.Normal, p3.Normal) * -p1.D +
|
|
Vector3.Cross(p3.Normal, p1.Normal) * -p2.D +
|
|
Vector3.Cross(p1.Normal, p2.Normal) * -p3.D) / (float)denom;
|
|
|
|
if (Mathf.Abs(intersection.X) > maxDist * 1f || Mathf.Abs(intersection.Y) > maxDist * 1f ||
|
|
Mathf.Abs(intersection.Z) > maxDist * 1f)
|
|
continue;
|
|
|
|
if (Math.Abs(denom) < 0.0000000001)
|
|
continue;
|
|
//if (intersection.Length > maxDist*2f)
|
|
// continue;
|
|
|
|
// Flip Y and Z
|
|
/*var temp = intersection.Y;
|
|
intersection.Y = intersection.Z;
|
|
intersection.Z = temp;*/
|
|
|
|
//if (intersection.Length >= maxDist)
|
|
// temp = temp;
|
|
|
|
planePoints.Add(intersection);
|
|
}
|
|
|
|
// remove duplicate points
|
|
var planePoints3 = planePoints;
|
|
planePoints = new HashSet<Vector3>();
|
|
|
|
foreach (Vector3 p1 in planePoints3)
|
|
{
|
|
bool found = false;
|
|
foreach (Vector3 p2 in planePoints)
|
|
if (Mathf.Abs((p1 - p2).Length) < 0.00001f)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if (!found)
|
|
planePoints.Add(p1);
|
|
}
|
|
|
|
if (planePoints.Count != planePoints3.Count)
|
|
Console.Print("culled " + (planePoints3.Count - planePoints.Count) + " points while triangulation");
|
|
|
|
// pass 2: cull points behind clipping planes
|
|
var planePoints2 = planePoints;
|
|
planePoints = new HashSet<Vector3>();
|
|
|
|
foreach (Vector3 p in planePoints2)
|
|
{
|
|
bool front = true;
|
|
foreach (MapFacePlane brushPlane in brush.planes)
|
|
{
|
|
float dot = -Plane.DotCoordinate(brushPlane.plane, p);
|
|
|
|
if (dot < -0.01f)
|
|
{
|
|
front = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (front)
|
|
planePoints.Add(p);
|
|
}
|
|
|
|
if (planePoints.Count > 0)
|
|
{
|
|
QuickHull(planePoints.ToArray(), out vertices);
|
|
return;
|
|
}
|
|
|
|
vertices = new Vector3[0];
|
|
}
|
|
|
|
#if false //FLAX_EDITOR
|
|
[OnSerializing]
|
|
internal void OnSerializing(StreamingContext context)
|
|
{
|
|
Debug.Log("OnSerializing: " + Editor.IsPlayMode);
|
|
}
|
|
|
|
[OnSerialized]
|
|
internal void OnSerialized(StreamingContext context)
|
|
{
|
|
Debug.Log("OnSerialized: " + Editor.IsPlayMode);
|
|
}
|
|
|
|
[OnDeserializing]
|
|
internal void OnDeserializing(StreamingContext context)
|
|
{
|
|
Debug.Log("OnDeserializing: " + Editor.IsPlayMode);
|
|
}
|
|
|
|
[OnDeserialized]
|
|
internal void OnDeserialized(StreamingContext context)
|
|
{
|
|
Debug.Log("OnDeserialized: " + Editor.IsPlayMode);
|
|
}
|
|
#endif
|
|
public override void OnStart()
|
|
{
|
|
#if false
|
|
Action onScriptsReloadBegin = null;
|
|
onScriptsReloadBegin = () =>
|
|
{
|
|
Debug.Log("LoadMap ScriptsReloadEnd");
|
|
Actor worldSpawnActor = Actor.GetChild("WorldSpawn");
|
|
if (worldSpawnActor != null)
|
|
{
|
|
Debug.Log("LoadMap: removing DontSave flag");
|
|
worldSpawnActor.HideFlags &= ~HideFlags.DontSave;
|
|
}
|
|
ScriptsBuilder.ScriptsReloadBegin -= onScriptsReloadBegin;
|
|
};
|
|
ScriptsBuilder.ScriptsReloadBegin += onScriptsReloadBegin;
|
|
|
|
Action onScriptsReloadEnd = null;
|
|
onScriptsReloadEnd = () =>
|
|
{
|
|
Debug.Log("LoadMap ScriptsReloadEnd");
|
|
Actor worldSpawnActor = Actor.GetChild("WorldSpawn");
|
|
if (worldSpawnActor != null)
|
|
{
|
|
Debug.Log("LoadMap: restoring DontSave flag");
|
|
worldSpawnActor.HideFlags |= HideFlags.DontSave;
|
|
}
|
|
ScriptsBuilder.ScriptsReloadEnd -= onScriptsReloadEnd;
|
|
};
|
|
ScriptsBuilder.ScriptsReloadEnd += onScriptsReloadEnd;
|
|
#endif
|
|
|
|
//Debug.Log("LoadMap");
|
|
LoadMap(false);
|
|
}
|
|
|
|
private void LoadMap(bool forceLoad)
|
|
{
|
|
Actor worldSpawnActor = Actor.GetChild("WorldSpawn");
|
|
if (worldSpawnActor != null)
|
|
{
|
|
if (forceLoad)
|
|
{
|
|
worldSpawnActor.DestroyChildren();
|
|
}
|
|
else
|
|
{
|
|
Console.Print("Map already loaded in the scene");
|
|
return;
|
|
}
|
|
}
|
|
|
|
{
|
|
string workDir = Directory.GetCurrentDirectory();
|
|
string matBasePath = Path.Combine(workDir, "Content", "Materials");
|
|
string assetPath = Path.Combine(matBasePath, "missing.flax");
|
|
missingMaterial = Content.Load<MaterialBase>(assetPath);
|
|
}
|
|
|
|
Stopwatch sw = Stopwatch.StartNew();
|
|
byte[] mapChars = File.ReadAllBytes(mapPath);
|
|
root = MapParser.Parse(mapChars);
|
|
sw.Stop();
|
|
|
|
//Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
bool oneMesh = false;
|
|
bool convexMesh = true;
|
|
|
|
if (!oneMesh)
|
|
{
|
|
var materials = new Dictionary<string, MaterialBase>();
|
|
{
|
|
BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy")
|
|
?.CreateInstance<BrushMaterialList>();
|
|
if (brushMaterialList != null)
|
|
foreach (BrushMaterialListEntry m in brushMaterialList.materialAssets)
|
|
materials.Add(m.name, m.asset);
|
|
}
|
|
|
|
if (worldSpawnActor == null)
|
|
{
|
|
worldSpawnActor = Actor.AddChild<Actor>();
|
|
worldSpawnActor.Name = "WorldSpawn";
|
|
worldSpawnActor.HideFlags |= HideFlags.DontSave;
|
|
//worldSpawnActor.HideFlags |= HideFlags.DontSelect;
|
|
}
|
|
|
|
var brushGeometries = new List<BrushGeometry>(root.entities[0].brushes.Count);
|
|
|
|
// pass 1: triangulation
|
|
sw.Restart();
|
|
int brushIndex = 0;
|
|
int totalverts = 0;
|
|
foreach (MapBrush brush in root.entities[0].brushes)
|
|
{
|
|
try
|
|
{
|
|
BrushGeometry geom = new BrushGeometry();
|
|
|
|
TriangulateBrush(brush, out geom.vertices);
|
|
geom.brush = brush;
|
|
|
|
brushGeometries.Add(geom);
|
|
totalverts += geom.vertices.Length;
|
|
|
|
Assert.IsTrue(geom.vertices.Length > 0);
|
|
|
|
foreach (Vector3 vert in geom.vertices)
|
|
geom.offset += vert;
|
|
geom.offset /= geom.vertices.Length;
|
|
|
|
for (int i = 0; i < geom.vertices.Length; i++)
|
|
geom.vertices[i] -= geom.offset;
|
|
|
|
var brushMaterials = new Dictionary<string, MaterialBase>();
|
|
foreach (MapFacePlane brushPlane in geom.brush.planes)
|
|
{
|
|
string textureName = brushPlane.texture;
|
|
if (brushMaterials.ContainsKey(textureName))
|
|
continue;
|
|
|
|
if (!materials.TryGetValue(textureName, out MaterialBase brushMaterial))
|
|
{
|
|
string workDir = Directory.GetCurrentDirectory();
|
|
string matBasePath = Path.Combine(workDir, "Content", "Materials");
|
|
string assetPath = Path.Combine(matBasePath, textureName + ".flax");
|
|
brushMaterial = Content.Load<MaterialBase>(assetPath);
|
|
if (brushMaterial != null)
|
|
{
|
|
materials.Add(textureName, brushMaterial);
|
|
}
|
|
else
|
|
{
|
|
// TODO: engine doesn't seem to always load the asset even though it exists, bug? seems to happen at low framerate
|
|
//Console.Print("Material '" + textureName + "' not found for brush, assetPath: " + assetPath);
|
|
materials.Add(textureName, missingMaterial);
|
|
brushMaterial = missingMaterial;
|
|
}
|
|
}
|
|
|
|
brushMaterials.Add(textureName, brushMaterial);
|
|
}
|
|
|
|
geom.brushMaterials = brushMaterials;
|
|
geom.meshes = new BrushGeometryMesh[brushMaterials.Count];
|
|
for (int i = 0; i < geom.meshes.Length; i++)
|
|
{
|
|
geom.meshes[i] = new BrushGeometryMesh();
|
|
geom.meshes[i].material = geom.brushMaterials[geom.brushMaterials.Keys.ToList()[i]];
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.Print("Failed to triangulate brush " + brushIndex + ": " + e.Message);
|
|
//FlaxEngine.Engine.RequestExit();
|
|
}
|
|
|
|
brushIndex++;
|
|
}
|
|
|
|
sw.Stop();
|
|
//Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
// pass 2: texturing
|
|
brushIndex = 0;
|
|
sw.Restart();
|
|
foreach (BrushGeometry geom in brushGeometries)
|
|
{
|
|
var brushVertices = geom.vertices;
|
|
for (int i = 0; i < brushVertices.Length; i += 3)
|
|
{
|
|
Vector3 v1 = brushVertices[i + 0];
|
|
Vector3 v2 = brushVertices[i + 1];
|
|
Vector3 v3 = brushVertices[i + 2];
|
|
|
|
Vector3 normal = -Vector3.Cross(v3 - v1, v2 - v1).Normalized;
|
|
|
|
// fetch the texture parameters from the plane with matching normal
|
|
Vector2 uvScale = new Vector2(0f);
|
|
float uvRotation = 0f;
|
|
Vector2 uvOffset = new Vector2(0f);
|
|
bool found = false;
|
|
int meshIndex = 0;
|
|
foreach (MapFacePlane brushPlane in geom.brush.planes)
|
|
if ((brushPlane.plane.Normal - normal).Length < 0.01f)
|
|
{
|
|
normal = brushPlane.plane.Normal; // for consistency
|
|
uvScale = 1f / brushPlane.scale;
|
|
uvRotation = brushPlane.rotation;
|
|
uvOffset = brushPlane.offset * brushPlane.scale;
|
|
found = true;
|
|
meshIndex = geom.brushMaterials.Keys.ToList().IndexOf(brushPlane.texture); // ugh?
|
|
break;
|
|
}
|
|
|
|
if (!found)
|
|
Console.Print("no matching plane found for brush " + brushIndex + ", bad geometry?");
|
|
|
|
Vector2 uv1, uv2, uv3;
|
|
// if quake format
|
|
{
|
|
// The texture is projected to the surface from three angles, the axis with least
|
|
// distortion is chosen here.
|
|
|
|
// Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis.
|
|
// This behaviour is seemingly random in different engines and editors, so let's not bother.
|
|
|
|
Vector3 textureNormal = new Vector3((float)Math.Round(normal.X, 4),
|
|
(float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4));
|
|
|
|
float dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right));
|
|
float dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up));
|
|
float dotZ = Math.Abs(Vector3.Dot(textureNormal, Vector3.Forward));
|
|
|
|
Vector3 axis;
|
|
if (dotY >= dotX && dotY >= dotZ)
|
|
axis = -Vector3.Up;
|
|
else if (dotX >= dotY && dotX >= dotZ)
|
|
axis = Vector3.Right;
|
|
else if (dotZ >= dotX && dotZ >= dotY)
|
|
axis = -Vector3.Forward;
|
|
else
|
|
axis = Vector3.Right;
|
|
|
|
Vector3 axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f
|
|
? -Vector3.Forward
|
|
: Vector3.Up;
|
|
Vector3 axisForward2 = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f
|
|
? Vector3.Up
|
|
: -Vector3.Forward;
|
|
|
|
Quaternion rot = Quaternion.Identity;
|
|
rot = rot * Quaternion.LookRotation(axis, axisForward);
|
|
rot = rot * Quaternion.RotationAxis(-Vector3.Forward,
|
|
180f * Mathf.DegreesToRadians);
|
|
rot = rot * Quaternion.RotationAxis(
|
|
Mathf.Abs(Vector3.Dot(axis, Vector3.Right)) > 0.01f
|
|
? Vector3.Right
|
|
: axisForward2,
|
|
uvRotation * Mathf.DegreesToRadians);
|
|
|
|
uv1 = ((Vector2)((v1 + geom.offset) * rot) + uvOffset) * uvScale;
|
|
uv2 = ((Vector2)((v2 + geom.offset) * rot) + uvOffset) * uvScale;
|
|
uv3 = ((Vector2)((v3 + geom.offset) * rot) + uvOffset) * uvScale;
|
|
}
|
|
BrushGeometryMesh mesh = geom.meshes[meshIndex];
|
|
|
|
mesh.indices.Add((uint)mesh.vertices.Count + 0);
|
|
mesh.indices.Add((uint)mesh.vertices.Count + 1);
|
|
mesh.indices.Add((uint)mesh.vertices.Count + 2);
|
|
|
|
mesh.vertices.Add(v1);
|
|
mesh.vertices.Add(v2);
|
|
mesh.vertices.Add(v3);
|
|
|
|
mesh.uvs.Add(uv1);
|
|
mesh.uvs.Add(uv2);
|
|
mesh.uvs.Add(uv3);
|
|
|
|
mesh.normals.Add(normal);
|
|
mesh.normals.Add(normal);
|
|
mesh.normals.Add(normal);
|
|
}
|
|
|
|
geom.model = Content.CreateVirtualAsset<Model>();
|
|
geom.model.SetupLODs(new[] { geom.meshes.Length });
|
|
geom.model.SetupMaterialSlots(geom.meshes.Length);
|
|
|
|
for (int i = 0; i < geom.meshes.Length; i++)
|
|
{
|
|
BrushGeometryMesh mesh = geom.meshes[i];
|
|
if (mesh.vertices.Count == 0)
|
|
continue;
|
|
|
|
geom.model.LODs[0].Meshes[i].UpdateMesh(mesh.vertices, mesh.indices, mesh.normals,
|
|
null, mesh.uvs);
|
|
geom.model.LODs[0].Meshes[i].MaterialSlotIndex = i;
|
|
geom.model.MaterialSlots[i].Material = geom.meshes[i].material;
|
|
}
|
|
|
|
brushIndex++;
|
|
}
|
|
|
|
sw.Stop();
|
|
//Console.Print("Pass 2: texturing: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
// pass 3: collision
|
|
sw.Restart();
|
|
brushIndex = 0;
|
|
foreach (BrushGeometry geom in brushGeometries)
|
|
{
|
|
StaticModel childModel = worldSpawnActor.AddChild<StaticModel>();
|
|
childModel.Name = "Brush_" + brushIndex;
|
|
childModel.Model = geom.model;
|
|
childModel.Position = geom.offset;
|
|
|
|
for (int i = 0; i < geom.meshes.Length; i++)
|
|
childModel.SetMaterial(i, geom.meshes[i].material);
|
|
|
|
uint[] indices = new uint[geom.vertices.Length];
|
|
for (uint i = 0; i < indices.Length; i++)
|
|
indices[i] = i;
|
|
|
|
if (geom.meshes.Length == 1)
|
|
{
|
|
MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial");
|
|
if (info != null && (bool)info.Value)
|
|
{
|
|
var entries = childModel.Entries;
|
|
entries[0].Visible = false;
|
|
childModel.Entries = entries;
|
|
}
|
|
}
|
|
|
|
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
|
|
if (collisionData.CookCollision(
|
|
convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, geom.vertices,
|
|
indices))
|
|
{
|
|
bool failed = true;
|
|
if (convexMesh)
|
|
{
|
|
// fallback to triangle mesh
|
|
failed = collisionData.CookCollision(CollisionDataType.TriangleMesh,
|
|
geom.vertices,
|
|
indices);
|
|
if (!failed)
|
|
Console.PrintWarning("Hull brush " + brushIndex + " is not convex");
|
|
}
|
|
|
|
if (failed)
|
|
throw new Exception("failed to cook final collision");
|
|
}
|
|
|
|
MeshCollider meshCollider = childModel.AddChild<MeshCollider>();
|
|
meshCollider.CollisionData = collisionData;
|
|
brushIndex++;
|
|
}
|
|
|
|
sw.Stop();
|
|
//Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
}
|
|
else
|
|
{
|
|
var vertices = new List<Vector3>();
|
|
var uvs = new List<Vector2>();
|
|
var normals = new List<Vector3>();
|
|
|
|
sw.Restart();
|
|
int brushIndex = 0;
|
|
foreach (MapBrush brush in root.entities[0].brushes)
|
|
{
|
|
try
|
|
{
|
|
TriangulateBrush(brush, out var brushVertices);
|
|
var brushUvs = new Vector2[brushVertices.Length];
|
|
var brushNormals = new Vector3[brushVertices.Length];
|
|
|
|
for (int i = 0; i < brushVertices.Length; i += 3)
|
|
{
|
|
Vector3 v1 = brushVertices[i + 0];
|
|
Vector3 v2 = brushVertices[i + 1];
|
|
Vector3 v3 = brushVertices[i + 2];
|
|
|
|
Vector3 normal = -Vector3.Cross(v3 - v1, v2 - v1).Normalized;
|
|
|
|
// fetch the texture parameters from the plane with matching normal
|
|
Vector2 uvScale = new Vector2(0f);
|
|
float uvRotation = 0f;
|
|
Vector2 uvOffset = new Vector2(0f);
|
|
bool found = false;
|
|
foreach (MapFacePlane brushPlane in brush.planes)
|
|
if ((brushPlane.plane.Normal - normal).Length < 0.01f)
|
|
{
|
|
normal = brushPlane.plane.Normal; // for consistency
|
|
uvScale = 1f / brushPlane.scale;
|
|
uvRotation = brushPlane.rotation;
|
|
uvOffset = brushPlane.offset * brushPlane.scale;
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if (!found)
|
|
Console.Print("no matching plane found, bad geometry?");
|
|
|
|
Vector2 uv1, uv2, uv3;
|
|
// if quake format
|
|
{
|
|
// The texture is projected to the surface from three angles, the axis with least
|
|
// distortion is chosen here.
|
|
|
|
// Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis.
|
|
// This behaviour is seemingly random in different engines and editors, so let's not bother.
|
|
Vector3 textureNormal = new Vector3((float)Math.Round(normal.X, 4),
|
|
(float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4));
|
|
|
|
float dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right));
|
|
float dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up));
|
|
float dotZ = Math.Abs(Vector3.Dot(textureNormal, Vector3.Forward));
|
|
|
|
Vector3 axis;
|
|
if (dotY >= dotX && dotY >= dotZ)
|
|
axis = -Vector3.Up;
|
|
else if (dotX >= dotY && dotX >= dotZ)
|
|
axis = Vector3.Right;
|
|
else if (dotZ >= dotX && dotZ >= dotY)
|
|
axis = -Vector3.Forward;
|
|
else
|
|
axis = Vector3.Right;
|
|
|
|
Vector3 axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f
|
|
? -Vector3.Forward
|
|
: Vector3.Up;
|
|
Vector3 axisForward2 = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f
|
|
? Vector3.Up
|
|
: -Vector3.Forward;
|
|
|
|
Quaternion rot = Quaternion.Identity;
|
|
rot = rot * Quaternion.LookRotation(axis, axisForward);
|
|
rot = rot * Quaternion.RotationAxis(-Vector3.Forward,
|
|
180f * Mathf.DegreesToRadians);
|
|
rot = rot * Quaternion.RotationAxis(
|
|
Mathf.Abs(Vector3.Dot(axis, Vector3.Right)) > 0.01f
|
|
? Vector3.Right
|
|
: axisForward2,
|
|
uvRotation * Mathf.DegreesToRadians);
|
|
|
|
uv1 = ((Vector2)(v1 * rot) + uvOffset) * uvScale;
|
|
uv2 = ((Vector2)(v2 * rot) + uvOffset) * uvScale;
|
|
uv3 = ((Vector2)(v3 * rot) + uvOffset) * uvScale;
|
|
}
|
|
|
|
brushUvs[i + 0] = uv1;
|
|
brushUvs[i + 1] = uv2;
|
|
brushUvs[i + 2] = uv3;
|
|
|
|
brushNormals[i + 0] = normal;
|
|
brushNormals[i + 1] = normal;
|
|
brushNormals[i + 2] = normal;
|
|
}
|
|
|
|
vertices.AddRange(brushVertices);
|
|
uvs.AddRange(brushUvs);
|
|
normals.AddRange(brushNormals);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.Print("Failed to hull brush " + brushIndex + ": " + e.Message);
|
|
}
|
|
|
|
brushIndex++;
|
|
}
|
|
|
|
sw.Stop();
|
|
Console.Print("Pass 1: triangulation and texturing: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
sw.Restart();
|
|
if (vertices.Count > 0)
|
|
{
|
|
uint[] triangles = new uint[vertices.Count];
|
|
for (uint i = 0; i < vertices.Count; i++)
|
|
triangles[i] = i;
|
|
|
|
model = Content.CreateVirtualAsset<Model>();
|
|
model.SetupLODs(new[] { 1 });
|
|
model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(),
|
|
null, uvs.ToArray());
|
|
|
|
StaticModel childModel = Actor.AddChild<StaticModel>();
|
|
childModel.Name = "MapModel";
|
|
childModel.Model = model;
|
|
childModel.SetMaterial(0, missingMaterial);
|
|
|
|
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
|
|
if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(),
|
|
triangles.ToArray()))
|
|
throw new Exception("failed to cook final collision");
|
|
MeshCollider meshCollider = childModel.AddChild<MeshCollider>();
|
|
meshCollider.CollisionData = collisionData;
|
|
}
|
|
|
|
sw.Stop();
|
|
Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
}
|
|
|
|
// Handle entities
|
|
|
|
{
|
|
sw.Restart();
|
|
|
|
int lightIndex = 0;
|
|
int playerSpawnIndex = 0;
|
|
foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname")))
|
|
switch (entity.properties["classname"])
|
|
{
|
|
case "light":
|
|
if (importLights)
|
|
ParseLight(entity, worldSpawnActor, ref lightIndex);
|
|
break;
|
|
case "info_player_deathmatch":
|
|
ParsePlayerSpawn(entity, worldSpawnActor, ref playerSpawnIndex);
|
|
break;
|
|
}
|
|
|
|
Console.Print("entity parsing time: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
}
|
|
}
|
|
|
|
private void ParseLight(MapEntity entity, Actor worldSpawnActor, ref int lightIndex)
|
|
{
|
|
//Console.Print("light");
|
|
PointLight light = worldSpawnActor.AddChild<PointLight>();
|
|
light.Name = "Light_" + lightIndex;
|
|
light.LocalPosition = ParseOrigin(entity.properties["origin"]);
|
|
|
|
//"_color" "0.752941 0.752941 0"
|
|
//"light" "200"
|
|
|
|
if (entity.properties.TryGetValue("_color", out string colorStr))
|
|
light.Color = ParseColor(colorStr);
|
|
|
|
float lightamm = 200f;
|
|
if (entity.properties.TryGetValue("light", out string lightStr))
|
|
lightamm = float.Parse(lightStr);
|
|
|
|
|
|
light.Brightness = lightamm / 128f;
|
|
light.Layer = 1;
|
|
light.Radius = 1000f * 0.5f;
|
|
light.UseInverseSquaredFalloff = false;
|
|
light.FallOffExponent = 8;
|
|
light.ShadowsDistance = 2000f * 1f;
|
|
|
|
|
|
if (true)
|
|
{
|
|
// match FTEQW dynamic only light values
|
|
}
|
|
|
|
//Console.Print("light pos: " + light.Position);
|
|
|
|
lightIndex++;
|
|
}
|
|
|
|
private void ParsePlayerSpawn(MapEntity entity, Actor worldSpawnActor, ref int playerSpawnIndex)
|
|
{
|
|
Actor spawn = worldSpawnActor.AddChild<Actor>();
|
|
spawn.Name = "PlayerSpawn_" + playerSpawnIndex;
|
|
spawn.LocalPosition = ParseOrigin(entity.properties["origin"]);
|
|
|
|
string angleStr = entity.properties.ContainsKey("angle") ? entity.properties["angle"] : "0";
|
|
spawn.Orientation = ParseAngle(angleStr);
|
|
|
|
//spawn.Tag = "PlayerSpawn";
|
|
|
|
playerSpawnIndex++;
|
|
}
|
|
|
|
private static Vector3 ParseOrigin(string origin)
|
|
{
|
|
string[] points = origin.Split(' ');
|
|
return new Vector3(float.Parse(points[0]), float.Parse(points[2]), float.Parse(points[1]));
|
|
}
|
|
|
|
private static Color ParseColor(string origin)
|
|
{
|
|
string[] points = origin.Split(' ');
|
|
return new Color(float.Parse(points[0]), float.Parse(points[1]), float.Parse(points[2]));
|
|
}
|
|
|
|
private static Quaternion ParseAngle(string origin)
|
|
{
|
|
string[] angles = origin.Split(' ');
|
|
//Console.Print("parseangle: " + new Vector3(0f, float.Parse(angles[0]) + 45f, 0f).ToString());
|
|
return Quaternion.Euler(new Vector3(0f, float.Parse(angles[0]) + 90f, 0f));
|
|
}
|
|
|
|
private static Vector3 ParseAngleEuler(string origin)
|
|
{
|
|
string[] angles = origin.Split(' ');
|
|
return new Vector3(0f, float.Parse(angles[0]) + 45f, 0f);
|
|
}
|
|
|
|
public override void OnDestroy()
|
|
{
|
|
Destroy(ref model);
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
} |