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

958 lines
39 KiB
C#

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<uint> indices = new List<uint>();
public MaterialBase material;
public List<Vector3> normals = new List<Vector3>();
public List<Vector2> uvs = new List<Vector2>();
public List<Vector3> vertices = new List<Vector3>();
}
public class BrushGeometry
{
public MapBrush brush;
public Dictionary<string, MaterialBase> 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 dirtyLights = false;
private float brightnessMultiplier_ = 0.82f;
private float lightRadiusMultiplier_ = 9.45f;
private float fallOffExponent_ = 2.0f;
private float saturationMultiplier_ = 1.0f;
private List<MapEntity> lightEnts = new List<MapEntity>();
private Actor worldSpawnActor = null;
[Range(0.1f, 4f)]
public float BrightnessMultiplier
{
get => brightnessMultiplier_;
set { brightnessMultiplier_ = value; dirtyLights = true; }
}
[Range(0.1f, 40f)]
public float LightRadiusMultiplier
{
get => lightRadiusMultiplier_;
set { lightRadiusMultiplier_ = value; dirtyLights = true; }
}
[Range(2f, 8f)]
public float FallOffExponent
{
get => fallOffExponent_;
set { fallOffExponent_ = value; dirtyLights = true; }
}
[Range(0.01f, 1f)]
public float SaturationMultiplier
{
get => saturationMultiplier_;
set { saturationMultiplier_ = value; dirtyLights = true; }
}
private static void QuickHull(Vector3[] points, out Vector3[] outVertices)
{
var verts = new List<Vector3>();
var tris = new List<int>();
var normals = new List<Vector3>();
ConvexHullCalculator calc = new ConvexHullCalculator();
calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals);
var finalPoints = new List<Vector3>();
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 Vector3[] vertices)
{
var planePoints = new HashSet<Vector3>();
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 = 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<Vector3>();
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<Vector3>();
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;
}
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);
}
public override void OnUpdate()
{
if (dirtyLights)
{
if (worldSpawnActor == null || !worldSpawnActor || root == 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;
}
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");
dirtyLights = false;
return;
}
}
else
FlaxEngine.Debug.Log("Loading map");
{
string workDir = Directory.GetCurrentDirectory();
string matBasePath = Path.Combine(workDir, "Content", "Materials");
string assetPath = Path.Combine(matBasePath, "missing.flax");
missingMaterial = Content.Load<MaterialBase>(assetPath);
}
ConcurrentBag<Model> sdfModels = new ConcurrentBag<Model>();
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<Actor>();
worldSpawnActor.Name = "WorldSpawn";
worldSpawnActor.HideFlags |= HideFlags.DontSave;
//worldSpawnActor.HideFlags |= HideFlags.DontSelect;
}
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);
}
var 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 (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<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 workDir = Directory.GetCurrentDirectory();
string matBasePath = Path.Combine(workDir, "Content", "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)
{
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<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();
brushIndex = 0;
foreach (BrushGeometry geom in brushGeometries)
{
StaticModel childModel = worldSpawnActor.AddChild<StaticModel>();
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<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++;
}
sw.Stop();
//Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms");
}
else
{
var vertices = new List<Vector3>();
var uvs = new List<Vector2>();
var normals = new List<Vector3>();
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>();
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.SetMaterial(0, missingMaterial);
string workDir = Directory.GetCurrentDirectory();
string matBasePath = Path.Combine(workDir, "Content", "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");
}
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<PointLight>();
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<Actor>();
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();
}
}
}