using System; using System.Collections.Generic; using FlaxEngine; using System.IO; using System.Linq; using System.Runtime.Serialization; using FlaxEditor; using FlaxEngine.Assertions; using Console = Cabrito.Console; using Stopwatch = System.Diagnostics.Stopwatch; namespace Game { public class BrushGeometryMesh { public List vertices = new List(); public List indices = new List(); public List uvs = new List(); public List normals = new List(); public MaterialBase material; } public class BrushGeometry { public MapBrush brush; public Vector3[] vertices; // all vertices public Vector3 offset; public BrushGeometryMesh[] meshes; public Model model; public Dictionary brushMaterials; } [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"; Model model; private MaterialBase missingMaterial; static void QuickHull(Vector3[] points, out Vector3[] outVertices) { var verts = new List(); var tris = new List(); var normals = new List(); var calc = new ConvexHullCalculator(); calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); var finalPoints = new List(); foreach (var tri in tris) { finalPoints.Add(verts[tri]); } outVertices = finalPoints.ToArray(); //verts = new QuickHull().QuickHull2(points); //outVertices = verts.ToArray(); } private MapEntity root; private static IEnumerable> DifferentCombinations(IEnumerable 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))); } /// /// Triangulates the brush by calculating intersection points between triplets of planes. /// Does not work well with off-axis aligned planes. /// public static void TriangulateBrush(MapBrush brush, out Vector3[] vertices) { HashSet planePoints = new HashSet(); List planes = new List(); float maxDist = 0f; foreach (var 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) { 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 (denom < 0.0000000001) // continue; 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; 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(); foreach (var p1 in planePoints3) { bool found = false; foreach (var 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(); foreach (var p in planePoints2) { bool front = true; foreach (var brushPlane in brush.planes) { var 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 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; } } { var workDir = Directory.GetCurrentDirectory(); var matBasePath = Path.Combine(workDir, "Content", "Materials"); var assetPath = Path.Combine(matBasePath, "missing.flax"); missingMaterial = Content.Load(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) { Dictionary materials = new Dictionary(); { BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy") ?.CreateInstance(); if (brushMaterialList != null) { foreach (var m in brushMaterialList.materialAssets) materials.Add(m.name, m.asset); } } if (worldSpawnActor == null) { worldSpawnActor = Actor.AddChild(); worldSpawnActor.Name = "WorldSpawn"; //worldSpawnActor.HideFlags |= HideFlags.DontSave; worldSpawnActor.HideFlags |= HideFlags.DontSelect; } List brushGeometries = new List(root.entities[0].brushes.Count); // pass 1: triangulation sw.Restart(); int brushIndex = 0; int totalverts = 0; foreach (var 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 (var vert in geom.vertices) geom.offset += vert; geom.offset /= geom.vertices.Length; for (int i=0; i brushMaterials = new Dictionary(); foreach (var brushPlane in geom.brush.planes) { var textureName = brushPlane.texture; if (brushMaterials.ContainsKey(textureName)) continue; if (!materials.TryGetValue(textureName, out var brushMaterial)) { var workDir = Directory.GetCurrentDirectory(); var matBasePath = Path.Combine(workDir, "Content", "Materials"); var assetPath = Path.Combine(matBasePath, textureName + ".flax"); brushMaterial = Content.Load(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.ToString() + ": " + 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 (var 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 (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; 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)); 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 + geom.offset) * rot) + uvOffset) * uvScale; uv2 = ((Vector2)((v2 + geom.offset) * rot) + uvOffset) * uvScale; uv3 = ((Vector2)((v3 + geom.offset) * rot) + uvOffset) * uvScale; } var 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(); geom.model.SetupLODs(new int[] { geom.meshes.Length }); geom.model.SetupMaterialSlots(geom.meshes.Length); for (int i=0; i(); 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; CollisionData collisionData = Content.CreateVirtualAsset(); 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.ToString() + " is not convex"); } if (failed) throw new Exception("failed to cook final collision"); } var meshCollider = childModel.AddChild(); meshCollider.CollisionData = collisionData; brushIndex++; } sw.Stop(); Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); } else { List vertices = new List(); List uvs = new List(); List normals = new List(); sw.Restart(); int brushIndex = 0; foreach (var brush in root.entities[0].brushes) { try { TriangulateBrush(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++; } 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.SetupLODs(new int[] { 1 }); model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(), null, uvs.ToArray()); StaticModel childModel = Actor.AddChild(); childModel.Name = "MapModel"; childModel.Model = model; childModel.SetMaterial(0, missingMaterial); CollisionData collisionData = Content.CreateVirtualAsset(); if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(), triangles.ToArray())) throw new Exception("failed to cook final collision"); var meshCollider = childModel.AddChild(); meshCollider.CollisionData = collisionData; } sw.Stop(); Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms"); } // Handle entities { sw.Restart(); int lightIndex = 0; foreach (var lightEntity in root.entities.Where(x => x.properties.ContainsKey("classname") && x.properties["classname"] == "light")) { Vector3 ParseOrigin(string origin) { string[] points = origin.Split(new char[] { ' ' }); return new Vector3(float.Parse(points[0]), float.Parse(points[2]), float.Parse(points[1])); } Color ParseColor(string origin) { string[] points = origin.Split(new char[] { ' ' }); return new Color(float.Parse(points[0]), float.Parse(points[1]), float.Parse(points[2])); } //Console.Print("light"); PointLight light = worldSpawnActor.AddChild(); light.Name = "Light_" + lightIndex; light.LocalPosition = ParseOrigin(lightEntity.properties["origin"]); //"_color" "0.752941 0.752941 0" //"light" "200" if (lightEntity.properties.TryGetValue("_color", out string colorStr)) { //light.Color = ParseColor(colorStr); } float lightamm = 200f; if (lightEntity.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++; } Console.Print("test: " + (new Vector2(1f,0f) == Vector2.Zero)); Console.Print("light parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); } } public override void OnDestroy() { Destroy(ref model); base.OnDestroy(); } } }