1478 lines
61 KiB
C#
1478 lines
61 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";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<MapEntity> lightEnts = new List<MapEntity>();
|
|
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<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(); frf f
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#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<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;
|
|
}
|
|
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<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;
|
|
}
|
|
}
|
|
|
|
if (dirtyMap)
|
|
{
|
|
dirtyMap = false;
|
|
FlaxEngine.Debug.Log("StaticBatching changed, reloading map");
|
|
LoadMap(true);
|
|
}
|
|
|
|
if (dirtyLights)
|
|
{
|
|
dirtyLights = false;
|
|
foreach (var light in worldSpawnActor.GetChildren<Light>())
|
|
{
|
|
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<LevelScript>();
|
|
|
|
if (levelScript.MapName != mapPath)
|
|
return true;
|
|
|
|
DateTime timestamp = File.GetLastWriteTime(mapPath);
|
|
if (timestamp != levelScript.MapTimestamp)
|
|
return true;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
private void UnloadMap()
|
|
{
|
|
IEnumerable<Actor> GetChildrenRecursive(Actor actor)
|
|
{
|
|
foreach (var act in actor.GetChildren<Actor>())
|
|
{
|
|
yield return act;
|
|
|
|
foreach (var child in GetChildrenRecursive(act))
|
|
yield return child;
|
|
}
|
|
}
|
|
|
|
var virtualAssets = new List<BinaryAsset>();
|
|
var allActors = GetChildrenRecursive(worldSpawnActor).ToList();
|
|
virtualAssets.AddRange(allActors.OfType<StaticModel>().Where(x => x.Model != null && x.Model.IsVirtual).Select(x => x.Model));
|
|
virtualAssets.AddRange(allActors.OfType<MeshCollider>().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<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 && 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<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");
|
|
}
|
|
#if false
|
|
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");
|
|
}
|
|
#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<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();
|
|
}
|
|
}
|
|
} |