using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using FlaxEditor; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; using Console = Game.Console; using Debug = FlaxEngine.Debug; 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 Float3 offset; public Float3[] vertices; // all vertices } [ExecuteInEditMode] public class Q3MapImporter : Script { //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q1.map";u8 //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 bool childModelSdf = true; private Model model; private MaterialBase missingMaterial; private bool resetLights = false; private bool dirtyLights = false; private bool dirtyMap = false; private float brightnessMultiplier_ = 0.82f; private float lightRadiusMultiplier_ = 9.45f; private float fallOffExponent_ = 2.0f; private float saturationMultiplier_ = 1.0f; private float indirectLightMultiplier_ = 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; } } [Range(1f, 100f)] public float IndirectLightMultiplier { get => indirectLightMultiplier_; set { indirectLightMultiplier_ = value; resetLights = true; } } public bool StaticBatching { get => staticBatching; set { if (staticBatching == value) return; staticBatching = value; dirtyLights = true; dirtyMap = true; } } private static void QuickHull(Float3[] points, out Float3[] 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(); frf f } 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 Float3[] 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 = Float3.Dot(p1.Normal, Float3.Cross(p2.Normal, p3.Normal)); //if (denom < 0.0000000001) // continue; Float3 intersection = (Float3.Cross(p2.Normal, p3.Normal) * -p1.D + Float3.Cross(p3.Normal, p1.Normal) * -p2.D + Float3.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 (Float3 p1 in planePoints3) { bool found = false; foreach (Float3 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 (Float3 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 Float3[0]; } #if FLAX_EDITOR private void OnEditorPlayModeStart() { try { if (worldSpawnActor) worldSpawnActor.HideFlags &= ~HideFlags.DontSave; } catch (Exception e) { FlaxEngine.Debug.Log("OnEditorPlayModeStart error: " + e.Message); } } private void OnEditorPlayModeEnd() { try { if (worldSpawnActor) 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; } #if FLAX_EDITOR private void OnSceneUnloading(Scene scene, Guid sceneId) { if (Editor.Instance == null) return; if (Editor.Instance.StateMachine.CurrentState is FlaxEditor.States.ChangingScenesState) //if (!Editor.IsPlayMode) UnloadMap(); } #endif public override void OnDisable() { if (Editor.Instance == null) return; Editor.Instance.PlayModeBeginning -= OnEditorPlayModeStart; Editor.Instance.PlayModeEnd -= OnEditorPlayModeEnd; //UnloadMap(); } #endif public override void OnStart() { sceneLighting = lastSceneLighting = EngineSubsystem.SceneLighting == "1"; sceneShadows = lastSceneShadows = EngineSubsystem.SceneShadows == "1"; staticBatching = lastStaticBatching = EngineSubsystem.StaticBatch == "1"; globalIllumination = EngineSubsystem.GlobalIllumination == "1"; LoadMap(false); } private List brushGeometries; private bool lastSceneLighting = false; private bool lastSceneShadows = false; private bool lastStaticBatching = false; private bool lastGlobalIllumination = false; private bool sceneLighting = false; private bool sceneShadows = false; private bool staticBatching = false; private bool globalIllumination = false; public override void OnUpdate() { sceneLighting = EngineSubsystem.SceneLighting == "1"; if (lastSceneLighting != sceneLighting) { lastSceneLighting = sceneLighting; dirtyLights = true; } sceneShadows = EngineSubsystem.SceneShadows == "1"; if (lastSceneShadows != sceneShadows) { lastSceneShadows = sceneShadows; dirtyLights = true; } var staticBatching = EngineSubsystem.StaticBatch == "1"; if (lastStaticBatching != staticBatching) { lastStaticBatching = staticBatching; StaticBatching = staticBatching; } globalIllumination = EngineSubsystem.GlobalIllumination == "1"; if (lastGlobalIllumination != globalIllumination) { lastGlobalIllumination = globalIllumination; dirtyMap = true; } if (resetLights) { resetLights = false; if (worldSpawnActor == null || !worldSpawnActor || root == null) { Debug.Log("worldspawn or root is 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; } } if (dirtyMap) { dirtyMap = false; FlaxEngine.Debug.Log("StaticBatching changed, reloading map"); LoadMap(true); } if (dirtyLights) { dirtyLights = false; foreach (var light in worldSpawnActor.GetChildren()) { light.IsActive = sceneLighting; if (light is PointLight pointLight) pointLight.ShadowsStrength = sceneShadows ? 1.0f : 0.0f; } } } private static bool IsMapDirty(Actor worldSpawnActor, string mapPath) { #if FLAX_EDITOR LevelScript levelScript = worldSpawnActor.GetScript(); if (levelScript.MapName != mapPath) return true; DateTime timestamp = File.GetLastWriteTime(mapPath); if (timestamp != levelScript.MapTimestamp) return true; #endif return false; } private void UnloadMap() { IEnumerable GetChildrenRecursive(Actor actor) { foreach (var act in actor.GetChildren()) { yield return act; foreach (var child in GetChildrenRecursive(act)) yield return child; } } var virtualAssets = new List(); var allActors = GetChildrenRecursive(worldSpawnActor).ToList(); virtualAssets.AddRange(allActors.OfType().Where(x => x.Model != null && x.Model.IsVirtual).Select(x => x.Model)); virtualAssets.AddRange(allActors.OfType().Where(x => x.CollisionData != null && x.CollisionData.IsVirtual).Select(x => x.CollisionData)); foreach (var asset in virtualAssets) Content.UnloadAsset(asset); worldSpawnActor.DestroyChildren(); FlaxEngine.Object.Destroy(worldSpawnActor); worldSpawnActor = null; resetLights = false; #if FLAX_EDITOR Level.SceneUnloading -= OnSceneUnloading; #endif } private void LoadMap(bool forceLoad) { Stopwatch sw = Stopwatch.StartNew(); string mapPath_ = mapPath; if (!File.Exists(mapPath_)) mapPath_ = Path.Combine(Directory.GetCurrentDirectory(), mapPath); if (!File.Exists(mapPath_)) mapPath_ = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", mapPath); byte[] mapChars = File.ReadAllBytes(mapPath_); root = MapParser.Parse(mapChars); sw.Stop(); //Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); dirtyMap = false; worldSpawnActor = Actor.FindActor("WorldSpawn"); if (worldSpawnActor != null) { if (!forceLoad && !IsMapDirty(worldSpawnActor, mapPath)) return; FlaxEngine.Debug.Log($"Map dirty, reloading."); UnloadMap(); } #if FLAX_EDITOR Level.SceneUnloading += OnSceneUnloading; #endif //else // FlaxEngine.Debug.Log("No WorldSpawn, loading map"); 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(); bool oneMesh = false; bool useStaticBatching = StaticBatching; bool convexMesh = true; if (worldSpawnActor == null) { worldSpawnActor = Actor.AddChild(); worldSpawnActor.Name = "WorldSpawn"; worldSpawnActor.HideFlags &= ~HideFlags.DontSave; //worldSpawnActor.HideFlags |= HideFlags.DontSave; //worldSpawnActor.HideFlags |= HideFlags.DontSelect; } LevelScript levelScript = worldSpawnActor.GetScript() ?? worldSpawnActor.AddScript(); #if FLAX_EDITOR levelScript.MapTimestamp = File.GetLastWriteTime(mapPath); levelScript.MapName = mapPath; #endif 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); } 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 (Float3 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) { Float3 v1 = brushVertices[i + 0]; Float3 v2 = brushVertices[i + 1]; Float3 v3 = brushVertices[i + 2]; Float3 normal = -Float3.Cross(v3 - v1, v2 - v1).Normalized; // fetch the texture parameters from the plane with matching normal Float2 uvScale = new Float2(0f); float uvRotation = 0f; Float2 uvOffset = new Float2(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?"); Float2 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. Float3 textureNormal = new Float3((float)Math.Round(normal.X, 4), (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); float dotX = Math.Abs(Float3.Dot(textureNormal, Float3.Right)); float dotY = Math.Abs(Float3.Dot(textureNormal, Float3.Up)); float dotZ = Math.Abs(Float3.Dot(textureNormal, Float3.Forward)); Float3 axis; if (dotY >= dotX && dotY >= dotZ) axis = -Float3.Up; else if (dotX >= dotY && dotX >= dotZ) axis = Float3.Right; else if (dotZ >= dotX && dotZ >= dotY) axis = -Float3.Forward; else axis = Float3.Right; Float3 axisForward = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f ? -Float3.Forward : Float3.Up; Float3 axisForward2 = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f ? Float3.Up : -Float3.Forward; Quaternion rot = Quaternion.Identity; rot = rot * Quaternion.LookRotation(axis, axisForward); rot = rot * Quaternion.RotationAxis(-Float3.Forward, 180f * Mathf.DegreesToRadians); rot = rot * Quaternion.RotationAxis( Mathf.Abs(Float3.Dot(axis, Float3.Right)) > 0.01f ? Float3.Right : axisForward2, uvRotation * Mathf.DegreesToRadians); uv1 = ((Float2)((v1 + geom.offset) * rot) + uvOffset) * uvScale; uv2 = ((Float2)((v2 + geom.offset) * rot) + uvOffset) * uvScale; uv3 = ((Float2)((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(); if (!useStaticBatching) { brushIndex = 0; foreach (BrushGeometry geom in brushGeometries) { StaticModel childModel = worldSpawnActor.AddChild(); childModel.Name = "Brush_" + brushIndex; childModel.Model = geom.model; childModel.Position = geom.offset; //childModel.DrawModes = DrawPass.None; for (int i = 0; i < geom.meshes.Length; i++) childModel.SetMaterial(i, geom.meshes[i].material); BrushScript brushScript = childModel.AddScript(); 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++; } } else { // create brush holder actors and collision brushIndex = 0; foreach (BrushGeometry geom in brushGeometries) { Actor childModel; if (childModelSdf) { StaticModel staticModel = worldSpawnActor.AddChild(); staticModel.DrawModes = DrawPass.GlobalSDF | DrawPass.GlobalSurfaceAtlas; staticModel.Model = geom.model; childModel = staticModel; } else 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); BrushScript brushScript = childModel.AddScript(); 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 && childModelSdf) { MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); if (info != null && (bool)info.Value) { var staticModel = childModel as StaticModel; var entries = staticModel.Entries; entries[0].Visible = false; entries[0].ShadowsMode = ShadowsCastingMode.None; entries[0].ReceiveDecals = false; staticModel.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 (childModelSdf && !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++; } // collect batches brushIndex = 0; Dictionary> batches = new Dictionary>(); foreach (BrushGeometry geom in brushGeometries) { 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) isClipMaterial = true; if (geom.meshes[0].material == missingMaterial) isMissingMaterial = true; } var brushMaterial = geom.meshes[0].material; if (!isClipMaterial) { if (!batches.TryGetValue(brushMaterial, out var batchGeometries)) { batchGeometries = new List(); batches.Add(brushMaterial, batchGeometries); } batchGeometries.Add(geom); } /*{ 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); brushIndex++; } foreach (var kvp in batches) { List normals = new List(); List uvs = new List(); List vertices = new List(); List indices = new List(); uint indicesOffset = 0; foreach (BrushGeometry geom in kvp.Value) { for (int i = 0; i < geom.meshes[0].vertices.Count; i++) { var v = geom.meshes[0].vertices[i]; var n = geom.meshes[0].normals[i]; var uv = geom.meshes[0].uvs[i]; vertices.Add(v + geom.offset); uvs.Add(uv); normals.Add(n); indices.Add(indicesOffset); indicesOffset++; } } var batchModel = Content.CreateVirtualAsset(); batchModel.SetupLODs(new[] { 1 }); batchModel.SetupMaterialSlots(1); batchModel.LODs[0].Meshes[0].UpdateMesh(vertices, indices, normals, null, uvs); batchModel.LODs[0].Meshes[0].MaterialSlotIndex = 0; batchModel.MaterialSlots[0].Material = kvp.Key; StaticModel childModel = worldSpawnActor.AddChild(); childModel.Name = "Batch_" + kvp.Key.Path; childModel.Model = batchModel; //childModel.Position = geom.offset; if (!childModelSdf) sdfModels.Add(batchModel); } } sw.Stop(); //Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); } #if false 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 Float2[brushVertices.Length]; var brushNormals = new Float3[brushVertices.Length]; for (int i = 0; i < brushVertices.Length; i += 3) { Float3 v1 = brushVertices[i + 0]; Float3 v2 = brushVertices[i + 1]; Float3 v3 = brushVertices[i + 2]; Float3 normal = -Float3.Cross(v3 - v1, v2 - v1).Normalized; // fetch the texture parameters from the plane with matching normal Float2 uvScale = new Float2(0f); float uvRotation = 0f; Float2 uvOffset = new Float2(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?"); Float2 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. Float3 textureNormal = new Float3((float)Math.Round(normal.X, 4), (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); float dotX = Math.Abs(Float3.Dot(textureNormal, Float3.Right)); float dotY = Math.Abs(Float3.Dot(textureNormal, Float3.Up)); float dotZ = Math.Abs(Float3.Dot(textureNormal, Float3.Forward)); Float3 axis; if (dotY >= dotX && dotY >= dotZ) axis = -Float3.Up; else if (dotX >= dotY && dotX >= dotZ) axis = Float3.Right; else if (dotZ >= dotX && dotZ >= dotY) axis = -Float3.Forward; else axis = Float3.Right; Float3 axisForward = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f ? -Float3.Forward : Float3.Up; Float3 axisForward2 = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f ? Float3.Up : -Float3.Forward; Quaternion rot = Quaternion.Identity; rot = rot * Quaternion.LookRotation(axis, axisForward); rot = rot * Quaternion.RotationAxis(-Float3.Forward, 180f * Mathf.DegreesToRadians); rot = rot * Quaternion.RotationAxis( Mathf.Abs(Float3.Dot(axis, Float3.Right)) > 0.01f ? Float3.Right : axisForward2, uvRotation * Mathf.DegreesToRadians); uv1 = ((Float2)(v1 * rot) + uvOffset) * uvScale; uv2 = ((Float2)(v2 * rot) + uvOffset) * uvScale; uv3 = ((Float2)(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.DrawModes = DrawPass.None; //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"); } #endif // 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"); } /*for (int i=0; i<10000; i++) { Debug.Log($"{i} udfghjosa fuhoag guiha7 2382835yayhahn0 generate:{generateSdf}, GI:{Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None}, {sdfModels.Count}"); }*/ //Debug.Log($"generate:{generateSdf}, GI:{Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None}, {sdfModels.Count}"); if (generateSdf && globalIllumination /*&& Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None*/ && sdfModels.Count > 1) { int modelIndex = 0; // TODO: read sdf data from texture and dump it to file, and reuse it when generating sdf data #if USE_NETCORE string mapHash = SHA1.HashData(Encoding.UTF8.GetBytes(levelScript.MapName + levelScript.MapTimestamp.Ticks.ToString())).ToString(); #else /*using*/ var sha1 = new SHA1Managed(); string mapHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(levelScript.MapName + levelScript.MapTimestamp.Ticks.ToString())).ToString(); #endif foreach (var model in sdfModels.ToList()) { string sdfDataPath = Path.Combine(AssetManager.CachePath, "MapSdfData", $"{mapHash}_brush{modelIndex+1}"); /*if (File.Exists(sdfDataPath)) { sdfModels.TryTake(out var model_); T RawDeserialize(byte[] rawData, int position) { int rawsize = Marshal.SizeOf(typeof(T)); if (rawsize > rawData.Length - position) throw new ArgumentException("Not enough data to fill struct. Array length from position: " + (rawData.Length - position) + ", Struct length: " + rawsize); IntPtr buffer = Marshal.AllocHGlobal(rawsize); Marshal.Copy(rawData, position, buffer, rawsize); T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T)); Marshal.FreeHGlobal(buffer); return retobj; } ModelBase.SDFData sdfData = new ModelBase.SDFData(); sdfData.Texture = GPUDevice.Instance.CreateTexture(sdfDataPath); if (sdfData.Texture.Init(new GPUTextureDescription() { Width = width, Height = height, Depth = depth, Format = format, Flags = GPUTextureFlags.ShaderResource, MipLevels = mips})) Console.PrintError($"Failed to create SDF texture for {sdfDataPath}"); sdfData.LocalToUVWMul = LocalToUVWMul; sdfData.LocalToUVWAdd = LocalToUVWAdd; sdfData.WorldUnitsPerVoxel = WorldUnitsPerVoxel; sdfData.MaxDistance = MaxDistance; sdfData.LocalBoundsMin = LocalBoundsMin; sdfData.LocalBoundsMax = LocalBoundsMax; sdfData.ResolutionScale = ResolutionScale; sdfData.LOD = LOD; for (int mipLevel = 0; mipLevel < mips; mipLevel++) { } //sdfData.Texture //sdfData.Texture model.SetSDF(sdfData); }*/ modelIndex++; } var task = Task.Run(() => { Stopwatch sw2 = Stopwatch.StartNew(); FlaxEngine.Debug.Log($"Generating level SDF ({sdfModels.Count} models)..."); Console.Print($"Generating level SDF ({sdfModels.Count} models)..."); ParallelOptions opts = new ParallelOptions(); FlaxEngine.Debug.Log("processorcount: " + Environment.ProcessorCount); float backfacesThreshold = 0.15f; if (useStaticBatching && !childModelSdf) { opts.MaxDegreeOfParallelism = 2; //Environment.ProcessorCount / 2; //backfacesThreshold = 1f; } Parallel.ForEach(sdfModels, opts, (model, _, index) => { if (model.WaitForLoaded()) throw new Exception("model was not loaded"); model.GenerateSDF(0.9f, 6, true, backfacesThreshold); /*byte[] RawSerialize(object anything) { int rawSize = Marshal.SizeOf(anything); IntPtr buffer = Marshal.AllocHGlobal(rawSize); Marshal.StructureToPtr(anything, buffer, false); byte[] rawDatas = new byte[rawSize]; Marshal.Copy(buffer, rawDatas, 0, rawSize); Marshal.FreeHGlobal(buffer); return rawDatas; } string sdfDataPath = Path.Combine(AssetManager.CachePath, "MapSdfData", $"{mapHash}_brush{modelIndex+1}");*/ }); sw2.Stop(); FlaxEngine.Debug.Log($"Generated level SDF in {sw2.Elapsed.TotalMilliseconds}ms"); Console.Print($"Generated level SDF in {sw2.Elapsed.TotalMilliseconds}ms"); }); } } private void ParseLight(MapEntity entity, ref int lightIndex) { LightWithShadow light; Float3? lightTargetPosition = null; int preset = 3; if (entity.properties.TryGetValue("target", out string targetName)) { var target = root.entities.FirstOrDefault(x => x.properties.ContainsKey("targetname") && x.properties["targetname"] == targetName); if (target != null) lightTargetPosition = ParseOrigin(target.properties["origin"]); } if (!lightTargetPosition.HasValue) light = worldSpawnActor.AddChild(); else light = worldSpawnActor.AddChild(); //Console.Print("light"); //PointLight light = worldSpawnActor.AddChild(); //LightWithShadow light = new PointLight(); var pointLight = light as PointLight; var spotLight = light as SpotLight; if (spotLight != null) light.Name = "SpotLight_" + lightIndex; else light.Name = "Light_" + lightIndex; light.IsActive = sceneLighting; light.LocalPosition = ParseOrigin(entity.properties["origin"]); if (lightTargetPosition.HasValue) light.Orientation = Quaternion.LookAt(light.LocalPosition, lightTargetPosition.Value); 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); bool castShadows = true; if (entity.properties.TryGetValue("castshadows", out string castShadowsStr)) castShadows = int.Parse(castShadowsStr) != 0; light.Layer = 1; if (pointLight != null) { pointLight.UseInverseSquaredFalloff = false; pointLight.FallOffExponent = 8; pointLight.ShadowsStrength = sceneShadows && castShadows ? 1.0f : 0.0f; } if (spotLight != null) { spotLight.UseInverseSquaredFalloff = false; spotLight.FallOffExponent = 8; spotLight.ShadowsStrength = sceneShadows && castShadows ? 1.0f : 0.0f; spotLight.InnerConeAngle = 65f; spotLight.OuterConeAngle = 80f; } light.ShadowsDistance = 500f; light.ShadowsDepthBias = 0.027f;//0.005f; if (preset == 0) // most accurate?, huge radius and low performance { light.Brightness = lightamm / 93f; light.ShadowsDepthBias = 0.0565f; light.Brightness *= 0.7837f; if (pointLight != null) { pointLight.Radius = radamm * 12.5f; pointLight.FallOffExponent = 3.33f; pointLight.Radius *= 0.83375f; } var hsv = light.Color.ToHSV(); hsv.Y *= 0.8f; light.Color = Color.FromHSV(hsv); } else if (preset == 1) // { if (pointLight != null) { pointLight.Radius = 250f; pointLight.FallOffExponent = 2f; } light.Brightness = (lightamm / 128f) * 1.25f; } else if (preset == 2) { if (pointLight != null) { pointLight.Radius = 200f; pointLight.FallOffExponent = 2f; } light.Brightness = (lightamm / 128f) * 1.6f; } else //if (preset == 3) { if (pointLight != null) { pointLight.Radius = radamm * LightRadiusMultiplier; pointLight.FallOffExponent = FallOffExponent; } if (spotLight != null) { spotLight.Radius = radamm * LightRadiusMultiplier; spotLight.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.IndirectLightingIntensity = IndirectLightMultiplier; light.ShadowsDepthBias = 0.0565f; // if low quality shadows //light.ShadowsDepthBias = 0.2492f; if (spotLight != null) { // huge aliasing with spot lights for some reason? light.ShadowsDepthBias = 0.7f; } } 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 Float3 ParseOrigin(string origin) { string[] points = origin.Split(' '); return new Float3(float.Parse(points[0], CultureInfo.InvariantCulture), float.Parse(points[2], CultureInfo.InvariantCulture), float.Parse(points[1], CultureInfo.InvariantCulture)); } private static Color ParseColor(string origin) { string[] points = origin.Split(' '); return new Color(float.Parse(points[0], CultureInfo.InvariantCulture), float.Parse(points[1], CultureInfo.InvariantCulture), float.Parse(points[2], CultureInfo.InvariantCulture)); } private static Quaternion ParseAngle(string origin) { string[] angles = origin.Split(' '); //Console.Print("parseangle: " + new Float3(0f, float.Parse(angles[0]) + 45f, 0f).ToString()); return Quaternion.Euler(new Float3(0f, float.Parse(angles[0], CultureInfo.InvariantCulture) + 90f, 0f)); } private static Float3 ParseAngleEuler(string origin) { string[] angles = origin.Split(' '); return new Float3(0f, float.Parse(angles[0], CultureInfo.InvariantCulture) + 45f, 0f); } public override void OnDestroy() { Destroy(ref model); base.OnDestroy(); } } }