Files
GoakeFlax/Source/Game/Level/Q3MapImporter.cs

1419 lines
59 KiB
C#

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<uint> indices = new List<uint>();
public MaterialBase material;
public List<Float3> normals = new List<Float3>();
public List<Float2> uvs = new List<Float2>();
public List<Float3> vertices = new List<Float3>();
}
public class BrushGeometry
{
public MapBrush brush;
public Dictionary<string, MaterialBase> 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";
//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<MapEntity> lightEnts = new List<MapEntity>();
private Actor worldSpawnActor = null;
private bool staticBatching_ = false;
[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<Float3>();
var tris = new List<int>();
var normals = new List<Float3>();
ConvexHullCalculator calc = new ConvexHullCalculator();
calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals);
var finalPoints = new List<Float3>();
foreach (int tri in tris)
finalPoints.Add(verts[tri]);
outVertices = finalPoints.ToArray();
//verts = new QuickHull().QuickHull2(points);
//outVertices = verts.ToArray();
}
private MapEntity root;
private static IEnumerable<IEnumerable<T>> DifferentCombinations<T>(IEnumerable<T> elements, int k)
{
return k == 0
? new[] { new T[0] }
: elements.SelectMany((e, i) =>
DifferentCombinations(elements.Skip(i + 1), k - 1).Select(c => new[] { e }.Concat(c)));
}
/// <summary>
/// Triangulates the brush by calculating intersection points between triplets of planes.
/// Does not work well with off-axis aligned planes.
/// </summary>
public static void TriangulateBrush(MapBrush brush, out Float3[] vertices)
{
var planePoints = new HashSet<Float3>();
var planes = new List<Plane>();
float maxDist = 0f;
foreach (MapFacePlane brushPlane in brush.planes)
{
if (Mathf.Abs(brushPlane.plane.D) > maxDist)
maxDist = Mathf.Abs(brushPlane.plane.D);
planes.Add(brushPlane.plane);
}
maxDist *= Mathf.Sqrt(3);
var combinations = DifferentCombinations(planes, 3).ToList();
// pass 1: get all intersection points
foreach (var comb in combinations)
{
Plane p1 = comb.Skip(0).First();
Plane p2 = comb.Skip(1).First();
Plane p3 = comb.Skip(2).First();
//var maxDist = Math.Abs(p1.D * p2.D * p3.D);//Math.Max(p1.D, Math.Max(p2.D, p3.D));
// intersection of three planes
double denom = 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<Float3>();
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<Float3>();
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;
}
public override void OnDisable()
{
if (Editor.Instance == null)
return;
Editor.Instance.PlayModeBeginning -= OnEditorPlayModeStart;
Editor.Instance.PlayModeEnd -= OnEditorPlayModeEnd;
}
#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<BrushGeometry> 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;
}
staticBatching = EngineSubsystem.StaticBatch == "1";
if (lastStaticBatching != staticBatching)
{
lastStaticBatching = staticBatching;
StaticBatching = staticBatching;
}
globalIllumination = EngineSubsystem.GlobalIllumination == "1";
if (lastGlobalIllumination != globalIllumination)
{
lastGlobalIllumination = globalIllumination;
dirtyMap = true;
}
if (resetLights)
{
if (worldSpawnActor == null || !worldSpawnActor || root == null)
{
Debug.Log("worldspawn or root is null");
return;
}
foreach (var light in worldSpawnActor.GetChildren<Light>())
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 (dirtyMap)
{
dirtyMap = false;
FlaxEngine.Debug.Log("StaticBatching changed, reloading map");
LoadMap(true);
}
if (dirtyLights)
{
foreach (var light in worldSpawnActor.GetChildren<Light>())
{
light.IsActive = sceneLighting;
if (light is PointLight pointLight)
pointLight.ShadowsStrength = sceneShadows ? 1.0f : 0.0f;
}
dirtyLights = false;
}
}
private static bool IsMapDirty(Actor worldSpawnActor, string mapPath)
{
#if FLAX_EDITOR
LevelScript levelScript = worldSpawnActor.GetScript<LevelScript>();
if (levelScript.MapName != mapPath)
return true;
DateTime timestamp = File.GetLastWriteTime(mapPath);
if (timestamp != levelScript.MapTimestamp)
return true;
#endif
return false;
}
private void LoadMap(bool forceLoad)
{
Stopwatch sw = Stopwatch.StartNew();
byte[] mapChars = File.ReadAllBytes(mapPath);
root = MapParser.Parse(mapChars);
sw.Stop();
//Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms");
worldSpawnActor = Actor.FindActor("WorldSpawn");
if (worldSpawnActor != null)
{
if (!forceLoad && !IsMapDirty(worldSpawnActor, mapPath))
return;
FlaxEngine.Debug.Log($"Map dirty, reloading.");
worldSpawnActor.DestroyChildren();
resetLights = false;
}
//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<MaterialBase>(assetPath);
}
ConcurrentBag<Model> sdfModels = new ConcurrentBag<Model>();
bool oneMesh = false;
bool useStaticBatching = StaticBatching;
bool convexMesh = true;
if (worldSpawnActor == null)
{
worldSpawnActor = Actor.AddChild<EmptyActor>();
worldSpawnActor.Name = "WorldSpawn";
worldSpawnActor.HideFlags &= ~HideFlags.DontSave;
//worldSpawnActor.HideFlags |= HideFlags.DontSave;
//worldSpawnActor.HideFlags |= HideFlags.DontSelect;
}
LevelScript levelScript =
worldSpawnActor.GetScript<LevelScript>() ?? worldSpawnActor.AddScript<LevelScript>();
#if FLAX_EDITOR
levelScript.MapTimestamp = File.GetLastWriteTime(mapPath);
levelScript.MapName = mapPath;
#endif
if (!oneMesh)
{
var materials = new Dictionary<string, MaterialBase>();
{
BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy")
?.CreateInstance<BrushMaterialList>();
if (brushMaterialList != null)
foreach (BrushMaterialListEntry m in brushMaterialList.materialAssets)
materials.Add(m.name, m.asset);
}
brushGeometries = new List<BrushGeometry>(root.entities[0].brushes.Count);
// pass 1: triangulation
sw.Restart();
int brushIndex = 0;
int totalverts = 0;
foreach (MapBrush brush in root.entities[0].brushes)
{
try
{
BrushGeometry geom = new BrushGeometry();
TriangulateBrush(brush, out geom.vertices);
geom.brush = brush;
brushGeometries.Add(geom);
totalverts += geom.vertices.Length;
Assert.IsTrue(geom.vertices.Length > 0);
foreach (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<string, MaterialBase>();
foreach (MapFacePlane brushPlane in geom.brush.planes)
{
string textureName = brushPlane.texture;
if (brushMaterials.ContainsKey(textureName))
continue;
if (!materials.TryGetValue(textureName, out MaterialBase brushMaterial))
{
string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials");
string assetPath = Path.Combine(matBasePath, textureName + ".flax");
brushMaterial = Content.Load<MaterialBase>(assetPath);
if (brushMaterial != null)
{
materials.Add(textureName, brushMaterial);
}
else
{
// TODO: engine doesn't seem to always load the asset even though it exists, bug? seems to happen at low framerate
Console.Print("Material '" + textureName + "' not found for brush, assetPath: " + assetPath);
materials.Add(textureName, missingMaterial);
brushMaterial = missingMaterial;
}
}
brushMaterials.Add(textureName, brushMaterial);
}
geom.brushMaterials = brushMaterials;
geom.meshes = new BrushGeometryMesh[brushMaterials.Count];
for (int i = 0; i < geom.meshes.Length; i++)
{
geom.meshes[i] = new BrushGeometryMesh();
geom.meshes[i].material = geom.brushMaterials[geom.brushMaterials.Keys.ToList()[i]];
}
}
catch (Exception e)
{
Console.Print("Failed to triangulate brush " + brushIndex + ": " + e.Message);
//FlaxEngine.Engine.RequestExit();
}
brushIndex++;
}
sw.Stop();
//Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms");
// pass 2: texturing
brushIndex = 0;
sw.Restart();
foreach (BrushGeometry geom in brushGeometries)
{
var brushVertices = geom.vertices;
for (int i = 0; i < brushVertices.Length; i += 3)
{
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<Model>();
geom.model.SetupLODs(new[] { geom.meshes.Length });
geom.model.SetupMaterialSlots(geom.meshes.Length);
for (int i = 0; i < geom.meshes.Length; i++)
{
BrushGeometryMesh mesh = geom.meshes[i];
if (mesh.vertices.Count == 0)
continue;
geom.model.LODs[0].Meshes[i].UpdateMesh(mesh.vertices, mesh.indices, mesh.normals,
null, mesh.uvs);
geom.model.LODs[0].Meshes[i].MaterialSlotIndex = i;
geom.model.MaterialSlots[i].Material = geom.meshes[i].material;
}
//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<StaticModel>();
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<BrushScript>();
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<CollisionData>();
if (collisionData.CookCollision(
convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh,
geom.vertices,
indices))
{
bool failed = true;
if (convexMesh)
{
// fallback to triangle mesh
failed = collisionData.CookCollision(CollisionDataType.TriangleMesh,
geom.vertices,
indices);
if (!failed)
Console.PrintWarning("Hull brush " + brushIndex + " is not convex");
}
if (failed)
throw new Exception("failed to cook final collision");
}
MeshCollider meshCollider = childModel.AddChild<MeshCollider>();
meshCollider.CollisionData = collisionData;
brushIndex++;
}
}
else
{
// create brush holder actors and collision
brushIndex = 0;
foreach (BrushGeometry geom in brushGeometries)
{
Actor childModel;
if (childModelSdf)
{
StaticModel staticModel = worldSpawnActor.AddChild<StaticModel>();
staticModel.DrawModes = DrawPass.GlobalSDF | DrawPass.GlobalSurfaceAtlas;
staticModel.Model = geom.model;
childModel = staticModel;
}
else
childModel = worldSpawnActor.AddChild<EmptyActor>();
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<BrushScript>();
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 (childModelSdf && !isClipMaterial && !isMissingMaterial)
sdfModels.Add(geom.model);
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
if (collisionData.CookCollision(
convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh,
geom.vertices,
indices))
{
bool failed = true;
if (convexMesh)
{
// fallback to triangle mesh
failed = collisionData.CookCollision(CollisionDataType.TriangleMesh,
geom.vertices,
indices);
if (!failed)
Console.PrintWarning("Hull brush " + brushIndex + " is not convex");
}
if (failed)
throw new Exception("failed to cook final collision");
}
MeshCollider meshCollider = childModel.AddChild<MeshCollider>();
meshCollider.CollisionData = collisionData;
brushIndex++;
}
// collect batches
brushIndex = 0;
Dictionary<MaterialBase, List<BrushGeometry>> batches = new Dictionary<MaterialBase, List<BrushGeometry>>();
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<BrushGeometry>();
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<Float3> normals = new List<Float3>();
List<Float2> uvs = new List<Float2>();
List<Float3> vertices = new List<Float3>();
List<uint> indices = new List<uint>();
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<Model>();
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<StaticModel>();
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");
}
else
{
var vertices = new List<Float3>();
var uvs = new List<Float2>();
var normals = new List<Float3>();
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>();
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<StaticModel>();
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<MaterialBase>(assetPath);
if (brushMaterial != null)
childModel.SetMaterial(0, brushMaterial);
else
childModel.SetMaterial(0, missingMaterial);
CollisionData collisionData = Content.CreateVirtualAsset<CollisionData>();
if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(),
triangles.ToArray()))
throw new Exception("failed to cook final collision");
MeshCollider meshCollider = childModel.AddChild<MeshCollider>();
meshCollider.CollisionData = collisionData;
}
sw.Stop();
Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms");
}
// Handle entities
{
sw.Restart();
int lightIndex = 0;
int playerSpawnIndex = 0;
foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname")))
switch (entity.properties["classname"])
{
case "light":
if (importLights)
ParseLight(entity, 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<T>(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<PointLight>();
else
light = worldSpawnActor.AddChild<SpotLight>();
//Console.Print("light");
//PointLight light = worldSpawnActor.AddChild<PointLight>();
//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<EmptyActor>();
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();
}
}
}