Files
FlaxEngine/Source/Editor/SceneGraph/Actors/StaticModelNode.cs
2024-04-05 16:37:11 +02:00

190 lines
8.0 KiB
C#

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#if USE_LARGE_WORLDS
using Real = System.Double;
#else
using Real = System.Single;
#endif
using System;
using System.Collections.Generic;
using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Windows;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
namespace FlaxEditor.SceneGraph.Actors
{
/// <summary>
/// Scene tree node for <see cref="StaticModel"/> actor type.
/// </summary>
/// <seealso cref="ActorNode" />
[HideInEditor]
public sealed class StaticModelNode : ActorNode
{
private Dictionary<IntPtr, Mesh.Vertex[]> _vertices;
/// <inheritdoc />
public StaticModelNode(Actor actor)
: base(actor)
{
}
/// <inheritdoc />
public override bool OnVertexSnap(ref Ray ray, Real hitDistance, out Vector3 result)
{
// Find the closest vertex to bounding box point (collision detection approximation)
result = ray.GetPoint(hitDistance);
var model = ((StaticModel)Actor).Model;
if (model && !model.WaitForLoaded())
{
// TODO: move to C++ and use cached vertex buffer internally inside the Mesh
if (_vertices == null)
_vertices = new();
var pointLocal = (Float3)Actor.Transform.WorldToLocal(result);
var minDistance = Real.MaxValue;
var lodIndex = 0; // TODO: use LOD index based on the game view
var lod = model.LODs[lodIndex];
{
var hit = false;
foreach (var mesh in lod.Meshes)
{
var key = FlaxEngine.Object.GetUnmanagedPtr(mesh);
if (!_vertices.TryGetValue(key, out var verts))
{
verts = mesh.DownloadVertexBuffer();
if (verts == null)
continue;
_vertices.Add(key, verts);
}
for (int i = 0; i < verts.Length; i++)
{
var v = verts[i].Position;
var distance = Float3.DistanceSquared(ref pointLocal, ref v);
if (distance <= minDistance)
{
hit = true;
minDistance = distance;
result = v;
}
}
}
if (hit)
{
result = Actor.Transform.LocalToWorld(result);
return true;
}
}
}
return false;
}
/// <inheritdoc />
public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window)
{
base.OnContextMenu(contextMenu, window);
contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null;
}
private void OnAddMeshCollider(EditorWindow window)
{
// Allow collider to be added to evey static model selection
var selection = Array.Empty<SceneGraphNode>();
if (window is SceneTreeWindow)
selection = Editor.Instance.SceneEditing.Selection.ToArray();
else if (window is PrefabWindow prefabWindow)
selection = prefabWindow.Selection.ToArray();
var createdNodes = new List<SceneGraphNode>();
foreach (var node in selection)
{
if (node is not StaticModelNode staticModelNode)
continue;
var model = ((StaticModel)staticModelNode.Actor).Model;
if (!model)
continue;
// Special case for in-built Editor models that can use analytical collision
var modelPath = model.Path;
if (modelPath.EndsWith("/Primitives/Cube.flax", StringComparison.Ordinal))
{
var actor = new BoxCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
if (modelPath.EndsWith("/Primitives/Sphere.flax", StringComparison.Ordinal))
{
var actor = new SphereCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
if (modelPath.EndsWith("/Primitives/Plane.flax", StringComparison.Ordinal))
{
var actor = new BoxCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
Size = new Float3(100.0f, 100.0f, 1.0f),
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
if (modelPath.EndsWith("/Primitives/Capsule.flax", StringComparison.Ordinal))
{
var actor = new CapsuleCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
Radius = 25.0f,
Height = 50.0f,
};
Editor.Instance.SceneEditing.Spawn(actor, staticModelNode.Actor);
actor.LocalPosition = new Vector3(0, 50.0f, 0);
actor.LocalOrientation = Quaternion.Euler(0, 0, 90.0f);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
// Create collision data (or reuse) and add collision actor
Action<CollisionData> created = collisionData =>
{
var actor = new MeshCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
CollisionData = collisionData,
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
};
var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy<CollisionData>();
collisionDataProxy.CreateCollisionDataFromModel(model, created, selection.Length == 1);
}
// Select all created nodes
if (window is SceneTreeWindow)
{
Editor.Instance.SceneEditing.Select(createdNodes);
}
else if (window is PrefabWindow pWindow)
{
pWindow.Select(createdNodes);
}
}
}
}