1053 lines
29 KiB
C#
1053 lines
29 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using FlaxEngine;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using FlaxEngine.Assertions;
|
|
using FlaxEngine.Utilities;
|
|
using Console = Cabrito.Console;
|
|
|
|
// TODO: remove coplanar/degenerate faces from the final mesh
|
|
// It seems the original algorithm is working but removing degenerate faces does not work
|
|
|
|
namespace Game
|
|
{
|
|
|
|
public class BrushGeometry
|
|
{
|
|
public MapBrush brush;
|
|
public Vector3[] vertices;
|
|
public uint[] indices;
|
|
public Vector2[] uvs;
|
|
public Vector3[] normals;
|
|
public Model model = null;
|
|
public MaterialBase brushMaterial = null; // FIXME: brush can have multiple textures
|
|
}
|
|
|
|
|
|
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\Goake\maps\aerowalk\aerowalk.map";
|
|
//private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\problematic.map";
|
|
|
|
Model model;
|
|
public MaterialBase material;
|
|
|
|
const float epsilon = 0.01f;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void QuickHull(Vector3[] points, out Vector3[] outVertices)
|
|
{
|
|
var verts = new List<Vector3>();
|
|
var tris = new List<int>();
|
|
var normals = new List<Vector3>();
|
|
|
|
var calc = new ConvexHullCalculator();
|
|
calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals);
|
|
|
|
//var qh = new QuickHull();
|
|
//verts = qh.QuickHull2(points);
|
|
|
|
var finalPoints = new List<Vector3>();
|
|
|
|
foreach (var tri in tris)
|
|
{
|
|
finalPoints.Add(verts[tri]);
|
|
}
|
|
|
|
outVertices = finalPoints.ToArray();
|
|
}
|
|
|
|
private Color[] planeColors = new Color[]
|
|
{
|
|
Color.Red,
|
|
Color.Orange,
|
|
Color.Yellow,
|
|
Color.Green,
|
|
Color.Cyan,
|
|
Color.Blue,
|
|
Color.Purple,
|
|
Color.Magenta,
|
|
};
|
|
|
|
private int skipPlanes = 0;
|
|
private int takePlanes = 5;
|
|
|
|
private List<Vector3> debugPoints = new List<Vector3>();
|
|
|
|
private List<Tuple<Vector3, Vector3>> debugNormals = new List<Tuple<Vector3, Vector3>>();
|
|
|
|
public override void OnDebugDraw()
|
|
{
|
|
foreach (var cn in debugNormals)
|
|
{
|
|
DebugDraw.DrawLine(cn.Item1, cn.Item1 + cn.Item2 * 50f, Color.Red, 0f, false);
|
|
}
|
|
return;
|
|
if (root == null)
|
|
return;
|
|
|
|
//foreach (var p in debugPoints)
|
|
// DebugDraw.DrawSphere(new BoundingSphere(p, 30f), Color.LightBlue, 0f, false);
|
|
|
|
foreach (var brush in root.entities[0].brushes.Skip(1).Take(1))
|
|
{
|
|
int planeI = skipPlanes;
|
|
foreach (var plane in brush.planes.Take(takePlanes))
|
|
//foreach (var plane in brush.planes)
|
|
{
|
|
Plane p = new Plane(plane.v1, plane.v2, plane.v3);
|
|
Vector3 planeNormal = -p.Normal;
|
|
|
|
const float w = 300f;
|
|
|
|
Vector3 p1 = new Vector3(-w, -w, 0f);
|
|
Vector3 p2 = new Vector3(w, -w, 0f);
|
|
Vector3 p3 = new Vector3(-w, w, 0f);
|
|
Vector3 p4 = new Vector3(w, w, 0f);
|
|
|
|
Vector3 uu = Vector3.Up;
|
|
if (Mathf.Abs(Vector3.Dot(planeNormal, uu)) > 0.9999f)
|
|
uu = Vector3.Forward;
|
|
|
|
var q = Quaternion.LookAt(Vector3.Zero, planeNormal, -uu);
|
|
|
|
p1 = p1 * q;
|
|
p2 = p2 * q;
|
|
p3 = p3 * q;
|
|
p4 = p4 * q;
|
|
|
|
p1 += p.D * planeNormal;
|
|
p2 += p.D * planeNormal;
|
|
p3 += p.D * planeNormal;
|
|
p4 += p.D * planeNormal;
|
|
|
|
var color = planeColors[planeI%planeColors.Length] * 0.5f;
|
|
DebugDraw.DrawTriangle(p1, p2, p3, color);
|
|
DebugDraw.DrawTriangle(p2, p3, p4, color);
|
|
|
|
|
|
|
|
planeI++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private MapEntity root;
|
|
|
|
private 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>
|
|
void TriangulateBrush(MapBrush brush, out Vector3[] vertices)
|
|
{
|
|
HashSet<Vector3> planePoints = new HashSet<Vector3>();
|
|
|
|
List<Plane> planes = new List<Plane>();
|
|
foreach (var brushPlane in brush.planes)
|
|
planes.Add(new Plane(brushPlane.v1, brushPlane.v2, brushPlane.v3));
|
|
|
|
var combinations = DifferentCombinations(planes, 3).ToList();
|
|
foreach (var comb in Enumerable.Reverse(combinations))
|
|
{
|
|
var p1 = comb.Skip(0).First();
|
|
var p2 = comb.Skip(1).First();
|
|
var 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 (Math.Abs(denom) < 0.000001f)
|
|
continue; // multiple or no intersections
|
|
|
|
if (Math.Abs(denom) < 0.000001f)
|
|
denom = denom;
|
|
|
|
var 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;
|
|
|
|
// Flip Y and Z
|
|
/*var temp = intersection.Y;
|
|
intersection.Y = intersection.Z;
|
|
intersection.Z = temp;*/
|
|
|
|
//if (intersection.Length >= maxDist)
|
|
// temp = temp;
|
|
|
|
planePoints.Add(intersection);
|
|
}
|
|
|
|
if (planePoints.Count > 0)
|
|
{
|
|
QuickHull(planePoints.ToArray(), out vertices);
|
|
return;
|
|
}
|
|
|
|
vertices = new Vector3[0];
|
|
}
|
|
|
|
Vector3[] TriangulateBrush2(MapBrush brush)
|
|
{
|
|
const float cs = 3000f;
|
|
|
|
Vector3[] cubePoints = new[]
|
|
{
|
|
new Vector3(-cs, -cs, -cs),
|
|
new Vector3(cs, -cs, -cs),
|
|
new Vector3(-cs, cs, -cs),
|
|
new Vector3(cs, cs, -cs),
|
|
new Vector3(-cs, -cs, cs),
|
|
new Vector3(cs, -cs, cs),
|
|
new Vector3(-cs, cs, cs),
|
|
new Vector3(cs, cs, cs),
|
|
};
|
|
Vector3[] cubeVerts;
|
|
QuickHull(cubePoints, out cubeVerts);
|
|
List<Vector3> brushVertices = new List<Vector3>(cubeVerts);
|
|
|
|
foreach (var brushPlane in brush.planes.Take(1))
|
|
{
|
|
Plane plane = new Plane(brushPlane.v1, brushPlane.v2, brushPlane.v3);
|
|
List<Vector3> faceVertices = new List<Vector3>();
|
|
List<Vector3> clippedVertices = new List<Vector3>();
|
|
|
|
Func<float, bool> isFront = (f) => f > epsilon;
|
|
Func<float, bool> isBack = (f) => f < -epsilon;
|
|
|
|
for (int i = 0; i < brushVertices.Count; i++)
|
|
{
|
|
int i2 = ((i + 1) % 3 == 0) ? (i - 2) : (i + 1);
|
|
Vector3 start = brushVertices[i];
|
|
Vector3 end = brushVertices[i2];
|
|
|
|
var d1 = Plane.DotCoordinate(plane, start);
|
|
var d2 = Plane.DotCoordinate(plane, end);
|
|
|
|
if (isBack(d1))
|
|
{
|
|
// include the point behind the clipping plane
|
|
faceVertices.Add(start);
|
|
}
|
|
|
|
if (isBack(d1) && isFront(d2) || isFront(d1) && isBack(d2))
|
|
{
|
|
// the cutting plane clips the edge
|
|
//if (isFront(d2))
|
|
{
|
|
Ray ray = new Ray(start, (end - start).Normalized);
|
|
if (plane.Intersects(ref ray, out Vector3 point))
|
|
{
|
|
|
|
//faceVertices.Add(point);
|
|
//clippedVertices.Add(point);
|
|
|
|
/*
|
|
intersect
|
|
start __._ (end)
|
|
| |/
|
|
| /
|
|
|/
|
|
next
|
|
*/
|
|
if (isBack(d1) && isFront(d2))
|
|
{
|
|
// finish the current triangle and start the next one
|
|
// [start, intersect, next], [intersect, ...]
|
|
|
|
faceVertices.Add(point);
|
|
|
|
if ((faceVertices.Count % 3) == 2)
|
|
{
|
|
int i3 = ((i2 + 1) % 3 == 0) ? (i2 - 2) : (i2 + 1);
|
|
Vector3 next = brushVertices[i3];
|
|
faceVertices.Add(next);
|
|
}
|
|
else
|
|
ray = ray;
|
|
|
|
faceVertices.Add(point);
|
|
}
|
|
|
|
/*
|
|
|
|
____ (start)
|
|
| |/
|
|
| * intersect2
|
|
|/
|
|
end
|
|
*/
|
|
else if (isFront(d1) && isBack(d2))
|
|
{
|
|
// continue where we left off
|
|
// [intersect, intersect2, ...]
|
|
|
|
faceVertices.Add(point);
|
|
|
|
if ((i % 3) == 2)
|
|
{
|
|
int i3 = ((i2 + 1) % 3 == 0) ? (i2 - 2) : (i2 + 1);
|
|
Vector3 next = brushVertices[i3];
|
|
faceVertices.Add(next);
|
|
}
|
|
else
|
|
ray = ray;
|
|
}
|
|
else
|
|
ray = ray;
|
|
}
|
|
else
|
|
d1 = d1;
|
|
}
|
|
}
|
|
}
|
|
|
|
brushVertices.Clear();
|
|
brushVertices.AddRange(faceVertices);
|
|
|
|
/*var newMeshPoints = new List<Vector3>();
|
|
int duplis = 0;
|
|
foreach (var v in faceVertices)
|
|
{
|
|
bool found = false;
|
|
foreach (var vo in newMeshPoints)
|
|
{
|
|
if ((v - vo).Length < epsilon)
|
|
{
|
|
found = true;
|
|
duplis++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//if (!newMeshPoints.Contains(v))
|
|
if (!found)
|
|
newMeshPoints.Add(v);
|
|
}
|
|
if (duplis > 0)
|
|
Console.Print("duplicates: " + duplis);
|
|
|
|
if (newMeshPoints.Count > 0)
|
|
{
|
|
var tempPoints = newMeshPoints;
|
|
newMeshPoints = new List<Vector3>(tempPoints.Count);
|
|
foreach (var tp in tempPoints)
|
|
{
|
|
// Flip Y and Z
|
|
newMeshPoints.Add(new Vector3(tp.X, tp.Z, tp.Y));
|
|
}
|
|
|
|
var hullPoints = QuickHull(newMeshPoints.ToArray());
|
|
var ms = new MeshSimplifier();
|
|
var optimizedVerts = ms.Simplify(hullPoints);
|
|
brushVertices.Clear();
|
|
brushVertices.AddRange(hullPoints);
|
|
}
|
|
else
|
|
brushVertices.Clear();
|
|
*/
|
|
|
|
}
|
|
|
|
return brushVertices.ToArray();
|
|
}
|
|
|
|
static public void TriangulateBrush3(MapBrush brush, out Vector3[] vertices)
|
|
{
|
|
float cs = 3000f;
|
|
|
|
float maxD = 0f;
|
|
float minD = 0f;
|
|
foreach (var brushPlane in brush.planes)
|
|
{
|
|
var p = brushPlane.plane;
|
|
minD = Mathf.Min(p.D);
|
|
maxD = Mathf.Max(p.D);
|
|
}
|
|
|
|
//cs = maxD*2;
|
|
|
|
|
|
Vector3[] cubePoints = new[]
|
|
{
|
|
new Vector3(-1, -1, -1) * cs,
|
|
new Vector3(1, -1, -1) * cs,
|
|
new Vector3(-1, 1, -1) * cs,
|
|
new Vector3(1, 1, -1) * cs,
|
|
new Vector3(-1, -1, 1) * cs,
|
|
new Vector3(1, -1, 1) * cs,
|
|
new Vector3(-1, 1, 1) * cs,
|
|
new Vector3(1, 1, 1) * cs,
|
|
};
|
|
Vector3[] cubeVerts;
|
|
QuickHull(cubePoints, out cubeVerts);
|
|
List<Vector3> brushVertices = new List<Vector3>(cubeVerts);
|
|
|
|
int asdf = 0;
|
|
//foreach (var brushPlane in brush.planes.Skip(skipPlanes).Take(takePlanes))
|
|
foreach (var brushPlane in brush.planes)
|
|
{
|
|
Plane plane = brushPlane.plane;
|
|
//if (asdf % 2 == 0)
|
|
//plane = new Plane(-plane.Normal, -plane.D);
|
|
|
|
|
|
List<Vector3> faceVertices = new List<Vector3>();
|
|
List<Vector3> clippedVertices = new List<Vector3>();
|
|
|
|
Func<float, bool> isFront = (f) => f > epsilon;
|
|
Func<float, bool> isBack = (f) => f < epsilon;
|
|
|
|
List<Tuple<Vector3, Vector3>> edges = new List<Tuple<Vector3, Vector3>>();
|
|
List<Tuple<Vector3, Vector3>> faceEdges = new List<Tuple<Vector3, Vector3>>();
|
|
|
|
void TriangulateEdges()
|
|
{
|
|
if (edges.Count > 0)
|
|
{
|
|
// heal discontinued edges
|
|
for (int j = 0; j < edges.Count; j++)
|
|
{
|
|
var edgePrev = edges[j];
|
|
var edgeNext = edges[(j + 1) % edges.Count];
|
|
|
|
//if (edgePrev.Item2 != edgeNext.Item1)
|
|
if ((edgePrev.Item2 - edgeNext.Item1).Length > 0.0001f)
|
|
{
|
|
var newEdge = new Tuple<Vector3, Vector3>(edgePrev.Item2, edgeNext.Item1);
|
|
edges.Insert(j + 1, newEdge);
|
|
j--;
|
|
}
|
|
}
|
|
|
|
// triangulate edges
|
|
for (int j = 0; j < edges.Count - 1; j++)
|
|
{
|
|
var edgePrev = edges[j];
|
|
var edgeNext = edges[(j + 1) % edges.Count];
|
|
|
|
Vector3 v0 = edges[0].Item1;
|
|
Vector3 v1 = edgePrev.Item2;
|
|
Vector3 v2 = edgeNext.Item2;
|
|
|
|
faceVertices.Add(v0);
|
|
faceVertices.Add(v1);
|
|
faceVertices.Add(v2);
|
|
}
|
|
|
|
// triangulate clipped face
|
|
/*for (int j = 0; j < clippedVertices.Count-1; j++)
|
|
{
|
|
Vector3 v0 = clippedVertices[0];
|
|
Vector3 v1 = edgePrev.Item2;
|
|
Vector3 v2 = edgeNext.Item2;
|
|
}*/
|
|
// TODO: maybe optimize the triangles here instead of using QuickHull
|
|
}
|
|
else
|
|
plane = plane;
|
|
|
|
edges.Clear();
|
|
}
|
|
|
|
for (int i = 0; i < brushVertices.Count; i++)
|
|
{
|
|
if (i > 0 && i % 3 == 0)
|
|
TriangulateEdges();
|
|
|
|
int i2 = ((i + 1) % 3 == 0) ? (i - 2) : (i + 1);
|
|
Vector3 start = brushVertices[i];
|
|
Vector3 end = brushVertices[i2];
|
|
|
|
var d1 = Plane.DotCoordinate(plane, start);
|
|
var d2 = Plane.DotCoordinate(plane, end);
|
|
|
|
Vector3 edgeStart = start;
|
|
Vector3 edgeEnd = end;
|
|
|
|
|
|
if (isBack(d1))
|
|
{
|
|
edgeStart = start;
|
|
}
|
|
|
|
if (isBack(d1) && isFront(d2) || isFront(d1) && isBack(d2))
|
|
{
|
|
Ray ray = new Ray(start, (end - start).Normalized);
|
|
Ray ray2 = new Ray(end, (start - end).Normalized);
|
|
if (plane.Intersects(ref ray, out Vector3 point) || plane.Intersects(ref ray2, out point))
|
|
{
|
|
edgeEnd = point;
|
|
clippedVertices.Add(point);
|
|
|
|
if (isFront(d1))
|
|
{
|
|
edgeStart = edgeEnd;
|
|
edgeEnd = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isFront(d1) && isFront(d2))
|
|
continue;
|
|
|
|
Tuple<Vector3, Vector3> edge = new Tuple<Vector3, Vector3>(edgeStart, edgeEnd);
|
|
edges.Add(edge);
|
|
}
|
|
|
|
TriangulateEdges();
|
|
if (false)
|
|
{
|
|
// create edges from clipped points
|
|
var clippedEdges = new List<Tuple<Vector3, Vector3>>();
|
|
|
|
//foreach (var e in edges)
|
|
// newEdges.Add(new Tuple<Vector3, Vector3>(e.Item1, e.Item2));
|
|
|
|
for (int i = 0; i < clippedVertices.Count; i++)
|
|
{
|
|
int i2 = (i + 1) % clippedVertices.Count;
|
|
Vector3 start = clippedVertices[i];
|
|
Vector3 end = clippedVertices[i2];
|
|
|
|
while (i < clippedVertices.Count)
|
|
{
|
|
int i3 = (i + 2) % clippedVertices.Count;
|
|
Vector3 end2 = clippedVertices[i3];
|
|
|
|
var edgeDirection = (end - start).Normalized;
|
|
var edgeDirection2 = (end2 - start).Normalized;
|
|
|
|
if ((edgeDirection2 - edgeDirection).Length < 0.0001f)
|
|
{
|
|
end = end2;
|
|
i++;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
clippedEdges.Add(new Tuple<Vector3, Vector3>(start, end));
|
|
}
|
|
|
|
// triangulate edges
|
|
for (int j = 0; j < clippedEdges.Count; j++)
|
|
{
|
|
var edgePrev = clippedEdges[j];
|
|
var edgeNext = clippedEdges[(j + 1) % clippedEdges.Count];
|
|
|
|
Vector3 v0 = clippedEdges[0].Item1;
|
|
Vector3 v1 = edgePrev.Item2;
|
|
Vector3 v2 = edgeNext.Item2;
|
|
|
|
faceVertices.Add(v0);
|
|
faceVertices.Add(v1);
|
|
faceVertices.Add(v2);
|
|
}
|
|
}
|
|
|
|
if (true)
|
|
{
|
|
List<Vector3> uniqPoints = new List<Vector3>();
|
|
foreach (var v in faceVertices)
|
|
{
|
|
bool found = false;
|
|
foreach (var v2 in uniqPoints)
|
|
{
|
|
if ((v - v2).Length < 0.01f)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
uniqPoints.Add(v);
|
|
//uniqPoints.Add(new Vector3((float)Math.Round(v.X, 3), (float)Math.Round(v.Y, 3), (float)Math.Round(v.Z, 3)));
|
|
}
|
|
|
|
//debugPoints = new List<Vector3>(uniqPoints);
|
|
|
|
|
|
Vector3[] hullPoints;
|
|
QuickHull(uniqPoints.ToArray(), out hullPoints);
|
|
|
|
var ms = new MeshSimplifier(1f, 7f);
|
|
var optimizedVerts = ms.Simplify(hullPoints);
|
|
|
|
brushVertices.Clear();
|
|
brushVertices.AddRange(optimizedVerts);
|
|
}
|
|
else
|
|
{
|
|
//debugPoints = new List<Vector3>(faceVertices);
|
|
|
|
var hullPoints = faceVertices;
|
|
|
|
var ms = new MeshSimplifier();
|
|
var optimizedVerts = hullPoints; //ms.Simplify(hullPoints);
|
|
|
|
brushVertices.Clear();
|
|
brushVertices.AddRange(optimizedVerts);
|
|
}
|
|
|
|
asdf++;
|
|
}
|
|
|
|
vertices = brushVertices.ToArray();
|
|
}
|
|
|
|
/*
|
|
Development (game)
|
|
cube3: 8.1ms + 123ms
|
|
aerowalk: 78ms + 1372ms
|
|
|
|
Development (editor)
|
|
cube3: 4.6ms + 77.3ms
|
|
aerowalk: 74ms + 1328ms
|
|
|
|
Release
|
|
cube3: 4.4ms + 61.4ms
|
|
aerowalk: 17ms + 511ms
|
|
|
|
UnitTest release:
|
|
aerowalk: 8ms + 267ms
|
|
|
|
aero unit:
|
|
.net6: 667 + 229
|
|
.net5: 704 + 237
|
|
.net5win7: 697 + 237
|
|
.net48 809 + 242
|
|
.net472 810 + 246
|
|
.net462 810 + 243
|
|
.net452 808 + 244
|
|
*/
|
|
|
|
public override void OnStart()
|
|
{
|
|
byte[] mapChars = File.ReadAllBytes(mapPath);
|
|
|
|
Stopwatch sw = Stopwatch.StartNew();
|
|
root = MapParser.Parse(mapChars);
|
|
sw.Stop();
|
|
|
|
Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
bool oneMesh = false;
|
|
bool convexMesh = true;
|
|
|
|
|
|
if (!oneMesh)
|
|
{
|
|
Dictionary<string, MaterialBase> materials = null;
|
|
var mapRootActor = Actor.AddChild<Actor>();
|
|
mapRootActor.Name = "MapRootActor";
|
|
|
|
List<BrushGeometry> brushGeometries = new List<BrushGeometry>(root.entities[0].brushes.Count);
|
|
|
|
|
|
// pass 1: triangulation
|
|
sw.Restart();
|
|
int brushIndex = 0;
|
|
foreach (var brush in root.entities[0].brushes)
|
|
{
|
|
try
|
|
{
|
|
BrushGeometry geom = new BrushGeometry();
|
|
|
|
TriangulateBrush3(brush, out geom.vertices);
|
|
geom.brush = brush;
|
|
|
|
brushGeometries.Add(geom);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.Print("Failed to triangulate brush " + brushIndex.ToString() + ": " + e.Message);
|
|
}
|
|
brushIndex++;
|
|
}
|
|
sw.Stop();
|
|
Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
// pass 2: texturing
|
|
sw.Restart();
|
|
foreach (var geom in brushGeometries)
|
|
{
|
|
var brushVertices = geom.vertices;
|
|
Vector2[] brushUvs = new Vector2[brushVertices.Length];
|
|
Vector3[] brushNormals = new Vector3[brushVertices.Length];
|
|
|
|
var textureName = geom.brush.planes[0].texture;
|
|
|
|
if (materials == null)
|
|
{
|
|
var customSettings = Engine.GetCustomSettings("BrushMaterials");
|
|
BrushMaterialList brushMaterialList = customSettings?.CreateInstance<BrushMaterialList>();
|
|
if (brushMaterialList != null)
|
|
{
|
|
materials = brushMaterialList.materialAssets.ToDictionary(x => x.name, y => y.asset);
|
|
Console.Print("materials dictionary with " + materials.Count + " entries");
|
|
}
|
|
else
|
|
{
|
|
materials = new Dictionary<string, MaterialBase>();
|
|
Console.Print("no materials dictionary found");
|
|
}
|
|
}
|
|
|
|
// FIXME: brush can have multiple textures
|
|
if (!materials.TryGetValue(textureName, out geom.brushMaterial))
|
|
{
|
|
Console.Print("Material '" + textureName + "' not found for brush");
|
|
materials.Add(textureName, material);
|
|
}
|
|
|
|
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 (var 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;
|
|
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));
|
|
|
|
var dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right));
|
|
var dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up));
|
|
var 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;
|
|
|
|
var axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f
|
|
? -Vector3.Forward
|
|
: Vector3.Up;
|
|
var 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;
|
|
}
|
|
|
|
geom.vertices = brushVertices;
|
|
geom.uvs = brushUvs;
|
|
geom.normals = brushNormals;
|
|
geom.indices = new uint[geom.vertices.Length];
|
|
for (uint i = 0; i < geom.vertices.Length; i++)
|
|
geom.indices[i] = i;
|
|
|
|
if (geom.vertices.Length > 0)
|
|
{
|
|
geom.model = Content.CreateVirtualAsset<Model>();
|
|
geom.model.SetupLODs(new int[] { 1 });
|
|
geom.model.LODs[0].Meshes[0].UpdateMesh(geom.vertices, geom.indices, geom.normals,
|
|
null, geom.uvs);
|
|
|
|
/*
|
|
StaticModel childModel = Actor.AddChild<StaticModel>();
|
|
childModel.Name = "Brush_" + brushIndex;
|
|
childModel.Model = geom.model;
|
|
childModel.SetMaterial(0, geom.brushMaterial);
|
|
childModel.Parent = mapRootActor;
|
|
|
|
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
|
|
if (collisionData.CookCollision(convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, geom.vertices.ToArray(),
|
|
geom.indices.ToArray()))
|
|
{
|
|
bool failed = true;
|
|
if (convexMesh)
|
|
{
|
|
// fallback to triangle mesh
|
|
failed = collisionData.CookCollision(CollisionDataType.TriangleMesh,
|
|
geom.vertices.ToArray(),
|
|
geom.indices.ToArray());
|
|
if (!failed)
|
|
Console.PrintWarning("Hull brush " + brushIndex.ToString() + " is not convex");
|
|
}
|
|
if (failed)
|
|
throw new Exception("failed to cook final collision");
|
|
}
|
|
|
|
var meshCollider = childModel.AddChild<MeshCollider>();
|
|
meshCollider.CollisionData = collisionData;
|
|
*/
|
|
}
|
|
|
|
//brushGeometries.Add(geom);
|
|
|
|
|
|
}
|
|
sw.Stop();
|
|
Console.Print("Pass 2: texturing: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
|
|
// pass 3: collision
|
|
sw.Restart();
|
|
foreach (var geom in brushGeometries)
|
|
{
|
|
StaticModel childModel = Actor.AddChild<StaticModel>();
|
|
childModel.Name = "Brush_" + brushIndex;
|
|
childModel.Model = geom.model;
|
|
childModel.SetMaterial(0, geom.brushMaterial);
|
|
childModel.Parent = mapRootActor;
|
|
|
|
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
|
|
if (collisionData.CookCollision(convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, geom.vertices,
|
|
geom.indices))
|
|
{
|
|
bool failed = true;
|
|
if (convexMesh)
|
|
{
|
|
// fallback to triangle mesh
|
|
failed = collisionData.CookCollision(CollisionDataType.TriangleMesh,
|
|
geom.vertices,
|
|
geom.indices);
|
|
if (!failed)
|
|
Console.PrintWarning("Hull brush " + brushIndex.ToString() + " is not convex");
|
|
}
|
|
if (failed)
|
|
throw new Exception("failed to cook final collision");
|
|
}
|
|
|
|
var meshCollider = childModel.AddChild<MeshCollider>();
|
|
meshCollider.CollisionData = collisionData;
|
|
}
|
|
sw.Stop();
|
|
Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms");
|
|
}
|
|
else
|
|
{
|
|
List<Vector3> vertices = new List<Vector3>();
|
|
List<Vector2> uvs = new List<Vector2>();
|
|
List<Vector3> normals = new List<Vector3>();
|
|
|
|
int brushIndex = 0;
|
|
foreach (var brush in root.entities[0].brushes)
|
|
{
|
|
try
|
|
{
|
|
TriangulateBrush3(brush, out Vector3[] brushVertices);
|
|
Vector2[] brushUvs = new Vector2[brushVertices.Length];
|
|
Vector3[] 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 (var 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));
|
|
|
|
var dotX = Math.Abs(Vector3.Dot(textureNormal, Vector3.Right));
|
|
var dotY = Math.Abs(Vector3.Dot(textureNormal, Vector3.Up));
|
|
var 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;
|
|
|
|
var axisForward = Mathf.Abs(Vector3.Dot(axis, Vector3.Up)) > 0.01f
|
|
? -Vector3.Forward
|
|
: Vector3.Up;
|
|
var 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.ToString() + ": " + e.Message);
|
|
}
|
|
|
|
brushIndex++;
|
|
}
|
|
|
|
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 int[] { 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, material);
|
|
|
|
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
|
|
if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(),
|
|
triangles.ToArray()))
|
|
throw new Exception("failed to cook final collision");
|
|
var meshCollider = childModel.AddChild<MeshCollider>();
|
|
meshCollider.CollisionData = collisionData;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnEnable()
|
|
{
|
|
// Here you can add code that needs to be called when script is enabled (eg. register for events)
|
|
}
|
|
|
|
public override void OnDisable()
|
|
{
|
|
// Here you can add code that needs to be called when script is disabled (eg. unregister from events)
|
|
}
|
|
|
|
public override void OnUpdate()
|
|
{
|
|
}
|
|
|
|
public override void OnDestroy()
|
|
{
|
|
Destroy(ref model);
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
} |