using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using FlaxEditor; using FlaxEngine; using FlaxEngine.Assertions; using Console = Game.Console; namespace Game { public class BrushGeometryMesh { public List indices = new List(); public MaterialBase material; public List normals = new List(); public List uvs = new List(); public List vertices = new List(); } public class BrushGeometry { public MapBrush brush; public Dictionary 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 bool generateSdf = true; private Model model; private MaterialBase missingMaterial; private bool resetLights = false; private bool dirtyLights = false; private float brightnessMultiplier_ = 0.82f; private float lightRadiusMultiplier_ = 9.45f; private float fallOffExponent_ = 2.0f; private float saturationMultiplier_ = 1.0f; private List lightEnts = new List(); private Actor worldSpawnActor = null; [Range(0.1f, 4f)] public float BrightnessMultiplier { get => brightnessMultiplier_; set { brightnessMultiplier_ = value; resetLights = true; } } [Range(0.1f, 40f)] public float LightRadiusMultiplier { get => lightRadiusMultiplier_; set { lightRadiusMultiplier_ = value; resetLights = true; } } [Range(2f, 8f)] public float FallOffExponent { get => fallOffExponent_; set { fallOffExponent_ = value; resetLights = true; } } [Range(0.01f, 1f)] public float SaturationMultiplier { get => saturationMultiplier_; set { saturationMultiplier_ = value; resetLights = true; } } private static void QuickHull(Vector3[] points, out Vector3[] outVertices) { var verts = new List(); var tris = new List(); var normals = new List(); ConvexHullCalculator calc = new ConvexHullCalculator(); calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); var finalPoints = new List(); 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> 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) { var planePoints = new HashSet(); var planes = new List(); 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(); 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(); 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 FLAX_EDITOR private void OnEditorPlayModeStart() { try { if (worldSpawnActor != null) worldSpawnActor.HideFlags &= ~HideFlags.DontSave; } catch (Exception e) { FlaxEngine.Debug.Log("OnEditorPlayModeStart error: " + e.Message); } } private void OnEditorPlayModeEnd() { try { if (worldSpawnActor != null) worldSpawnActor.HideFlags |= HideFlags.DontSave; dirtyLights = true; } catch (Exception e) { FlaxEngine.Debug.Log("OnEditorPlayModeEnd error: " + e.Message); } } public override void OnEnable() { Editor.Instance.PlayModeBeginning += OnEditorPlayModeStart; Editor.Instance.PlayModeEnd += OnEditorPlayModeEnd; } public override void OnDisable() { Editor.Instance.PlayModeBeginning -= OnEditorPlayModeStart; Editor.Instance.PlayModeEnd -= OnEditorPlayModeEnd; } #endif public override void OnStart() { LoadMap(false); } private bool lastSceneLighting = false; private bool lastSceneShadows = false; public override void OnUpdate() { bool sceneLighting = EngineSubsystem.SceneLighting == "1"; if (lastSceneLighting != sceneLighting) { lastSceneLighting = sceneLighting; dirtyLights = true; } bool sceneShadows = EngineSubsystem.SceneShadows == "1"; if (lastSceneShadows != sceneShadows) { lastSceneShadows = sceneShadows; dirtyLights = true; } if (resetLights) { if (worldSpawnActor == null || !worldSpawnActor || root == null) return; foreach (var light in worldSpawnActor.GetChildren()) Destroy(light); int lightIndex = 0; foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname"))) switch (entity.properties["classname"]) { case "light": if (importLights) ParseLight(entity, ref lightIndex); break; //case "info_player_deathmatch": // ParsePlayerSpawn(entity, ref playerSpawnIndex); // break; } resetLights = false; } if (dirtyLights) { foreach (var light in worldSpawnActor.GetChildren()) { light.IsActive = sceneLighting; if (light is PointLight pointLight) pointLight.ShadowsStrength = sceneShadows ? 1.0f : 0.0f; } dirtyLights = false; } } private void LoadMap(bool forceLoad) { worldSpawnActor = Actor.FindActor("WorldSpawn"); if (worldSpawnActor != null) { if (forceLoad) { FlaxEngine.Debug.Log("Destroying children"); worldSpawnActor.DestroyChildren(); } else { FlaxEngine.Debug.Log("Map already loaded in the scene"); resetLights = false; return; } } else FlaxEngine.Debug.Log("Loading map"); { string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); string assetPath = Path.Combine(matBasePath, "missing.flax"); missingMaterial = Content.Load(assetPath); } ConcurrentBag sdfModels = new ConcurrentBag(); 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 (worldSpawnActor == null) { worldSpawnActor = Actor.AddChild(); worldSpawnActor.Name = "WorldSpawn"; worldSpawnActor.HideFlags |= HideFlags.DontSave; //worldSpawnActor.HideFlags |= HideFlags.DontSelect; } if (!oneMesh) { var materials = new Dictionary(); { BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy") ?.CreateInstance(); if (brushMaterialList != null) foreach (BrushMaterialListEntry m in brushMaterialList.materialAssets) materials.Add(m.name, m.asset); } var brushGeometries = new List(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(); foreach (MapFacePlane brushPlane in geom.brush.planes) { string textureName = brushPlane.texture; if (brushMaterials.ContainsKey(textureName)) continue; if (!materials.TryGetValue(textureName, out MaterialBase brushMaterial)) { string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); string 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 + ": " + 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(); 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; } //Not supported yet, should be done here //geom.model.GenerateSDF(); brushIndex++; } sw.Stop(); //Console.Print("Pass 2: texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); // pass 3: static models & collision sw.Restart(); brushIndex = 0; foreach (BrushGeometry geom in brushGeometries) { StaticModel childModel = worldSpawnActor.AddChild(); 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; bool isClipMaterial = false; bool isMissingMaterial = false; 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; entries[0].ShadowsMode = ShadowsCastingMode.None; entries[0].ReceiveDecals = false; childModel.Entries = entries; isClipMaterial = true; } if (geom.meshes[0].material == missingMaterial) isMissingMaterial = true; } /*{ var entries = childModel.Entries; for (int i=0; i < entries.Length; i++) entries[i].Visible = false; childModel.Entries = entries; }*/ if (!isClipMaterial && !isMissingMaterial) sdfModels.Add(geom.model); 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 + " is not convex"); } if (failed) throw new Exception("failed to cook final collision"); } MeshCollider meshCollider = childModel.AddChild(); meshCollider.CollisionData = collisionData; brushIndex++; } sw.Stop(); //Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); } else { var vertices = new List(); var uvs = new List(); var normals = new List(); 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.SetupLODs(new[] { 1 }); model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(), null, uvs.ToArray()); sdfModels.Add(model); StaticModel childModel = worldSpawnActor.AddChild(); childModel.Name = "MapModel"; childModel.Model = model; //childModel.SetMaterial(0, missingMaterial); string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); string assetPath = Path.Combine(matBasePath, "dev/dev_128_gray" + ".flax"); var brushMaterial = Content.Load(assetPath); if (brushMaterial != null) childModel.SetMaterial(0, brushMaterial); else 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"); MeshCollider 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; 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, ref lightIndex); lightEnts.Add(entity); break; case "info_player_deathmatch": ParsePlayerSpawn(entity, ref playerSpawnIndex); break; } Console.Print("entity parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); } if (generateSdf && Graphics.EnableGlobalSDF && sdfModels.Count > 1) { var task = Task.Run(() => { Stopwatch sw = Stopwatch.StartNew(); Console.Print($"Generating level SDF ({sdfModels.Count} models)..."); Parallel.ForEach(sdfModels, new ParallelOptions() { MaxDegreeOfParallelism = 1 }, model => { if (model.WaitForLoaded()) throw new Exception("model was not loaded"); model.GenerateSDF(); }); Console.Print($"Generated level SDF in {sw.Elapsed.TotalMilliseconds}ms"); }); } } private void ParseLight(MapEntity entity, ref int lightIndex) { int preset = 3; //Console.Print("light"); PointLight light = worldSpawnActor.AddChild(); light.Name = "Light_" + lightIndex; light.LocalPosition = ParseOrigin(entity.properties["origin"]); if (entity.properties.TryGetValue("_color", out string colorStr)) light.Color = ParseColor(colorStr); float lightamm = 300f; if (entity.properties.TryGetValue("light", out string lightStr)) lightamm = float.Parse(lightStr); float radamm = 64f; if (entity.properties.TryGetValue("radius", out string radStr)) radamm = float.Parse(radStr); light.Layer = 1; light.UseInverseSquaredFalloff = false; light.FallOffExponent = 8; light.ShadowsDistance = 500f; light.ShadowsDepthBias = 0.027f;//0.005f; if (preset == 0) // most accurate?, huge radius and low performance { light.Brightness = lightamm / 93f; light.Radius = radamm * 12.5f; light.FallOffExponent = 3.33f; light.ShadowsDepthBias = 0.0565f; light.Brightness *= 0.7837f; light.Radius *= 0.83375f; var hsv = light.Color.ToHSV(); hsv.Y *= 0.8f; light.Color = Color.FromHSV(hsv); } else if (preset == 1) // { light.Radius = 250f; light.FallOffExponent = 2f; light.Brightness = (lightamm / 128f) * 1.25f; } else if (preset == 2) { light.Radius = 200f; light.FallOffExponent = 2f; light.Brightness = (lightamm / 128f) * 1.6f; } else //if (preset == 3) { light.Radius = radamm * LightRadiusMultiplier; light.FallOffExponent = FallOffExponent; light.Brightness = (lightamm / 128f) * BrightnessMultiplier; light.ShadowsNormalOffsetScale = 10f; light.ShadowsFadeDistance = 100f; // for debugging light.ShadowsDistance = 500f; var hsv = light.Color.ToHSV(); hsv.Y *= SaturationMultiplier; light.Color = Color.FromHSV(hsv); light.ShadowsDepthBias = 0.0565f; // if low quality shadows light.ShadowsDepthBias = 0.2492f; } lightIndex++; } private void ParsePlayerSpawn(MapEntity entity, ref int playerSpawnIndex) { Actor spawn = worldSpawnActor.AddChild(); 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(); } } }