// Copyright (c) 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 System.Linq; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEngine; namespace FlaxEditor.SceneGraph.Actors { /// /// Scene tree node for actor type. /// /// [HideInEditor] public sealed class StaticModelNode : ActorNode { private Dictionary _vertices; private Vector3[] _selectionPoints; private Transform _selectionPointsTransform; private Model _selectionPointsModel; /// /// Whether the model of the static model is one of the primitive models (box/sphere/capsule/etc.). /// public bool IsPrimitive { get { Model model = ((StaticModel)Actor).Model; if (!model) return false; string path = model.Path; return path.EndsWith("/Primitives/Cube.flax", StringComparison.Ordinal) || path.EndsWith("/Primitives/Sphere.flax", StringComparison.Ordinal) || path.EndsWith("/Primitives/Plane.flax", StringComparison.Ordinal) || path.EndsWith("/Primitives/Capsule.flax", StringComparison.Ordinal); } } /// public StaticModelNode(Actor actor) : base(actor) { } /// public override void OnDispose() { _vertices = null; _selectionPoints = null; _selectionPointsModel = null; base.OnDispose(); } /// 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)) { var accessor = new MeshAccessor(); if (accessor.LoadMesh(mesh)) continue; verts = accessor.Positions; if (verts == null) continue; _vertices.Add(key, verts); } for (int i = 0; i < verts.Length; i++) { ref var v = ref verts[i]; 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; } /// public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { base.OnContextMenu(contextMenu, window); // Check if every selected node is a primitive var selection = GetSelection(window); bool autoOptionEnabled = true; foreach (var node in selection) { if (node is StaticModelNode staticModelNode && !staticModelNode.IsPrimitive) { autoOptionEnabled = false; break; } } var menu = contextMenu.AddChildMenu("Add collider"); menu.Enabled = ((StaticModel)Actor).Model != null; var b = menu.ContextMenu.AddButton("Auto", () => OnAddCollider(window, CreateAuto)); b.TooltipText = "Add the best fitting collider to every model that uses an in-built Editor primitive."; b.Enabled = autoOptionEnabled; b = menu.ContextMenu.AddButton("Box", () => OnAddCollider(window, CreateBox)); b.TooltipText = "Add a box collider to every selected model that will auto resize based on the model bounds."; b = menu.ContextMenu.AddButton("Sphere", () => OnAddCollider(window, CreateSphere)); b.TooltipText = "Add a sphere collider to every selected model that will auto resize based on the model bounds."; b = menu.ContextMenu.AddButton("Capsule", () => OnAddCollider(window, CreateCapsule)); b.TooltipText = "Add a capsule collider to every selected model that will auto resize based on the model bounds."; b = menu.ContextMenu.AddButton("Convex", () => OnAddCollider(window, CreateConvex)); b.TooltipText = "Generate and add a convex collider for every selected model."; b = menu.ContextMenu.AddButton("Triangle Mesh", () => OnAddCollider(window, CreateTriangle)); b.TooltipText = "Generate and add a triangle mesh collider for every selected model."; } /// public override Vector3[] GetActorSelectionPoints() { if (Actor is StaticModel sm && sm.Model) { // Try to use cache var model = sm.Model; var transform = Actor.Transform; if (_selectionPoints != null && _selectionPointsTransform == transform && _selectionPointsModel == model) return _selectionPoints; Profiler.BeginEvent("GetActorSelectionPoints"); // Check collision proxy points for more accurate selection var vecPoints = new List(); var m = model.LODs[0]; foreach (var mesh in m.Meshes) { var points = mesh.GetCollisionProxyPoints(); vecPoints.EnsureCapacity(vecPoints.Count + points.Length); for (int i = 0; i < points.Length; i++) { vecPoints.Add(transform.LocalToWorld(points[i])); } } Profiler.EndEvent(); if (vecPoints.Count != 0) { _selectionPoints = vecPoints.ToArray(); _selectionPointsTransform = transform; _selectionPointsModel = model; return _selectionPoints; } } return base.GetActorSelectionPoints(); } private delegate void Spawner(Collider collider); private delegate void CreateCollider(StaticModel actor, Spawner spawner, bool singleNode); private IEnumerable GetSelection(EditorWindow window) { if (window is SceneTreeWindow) return Editor.Instance.SceneEditing.Selection; if (window is PrefabWindow prefabWindow) return prefabWindow.Selection; return Array.Empty(); } private void CreateAuto(StaticModel actor, Spawner spawner, bool singleNode) { // Special case for in-built Editor models that can use analytical collision Model model = actor.Model; var modelPath = model.Path; if (modelPath.EndsWith("/Primitives/Cube.flax", StringComparison.Ordinal)) { var collider = new BoxCollider { Transform = actor.Transform, }; spawner(collider); } else if (modelPath.EndsWith("/Primitives/Sphere.flax", StringComparison.Ordinal)) { var collider = new SphereCollider { Transform = actor.Transform, }; spawner(collider); collider.LocalTransform = Transform.Identity; } else if (modelPath.EndsWith("/Primitives/Plane.flax", StringComparison.Ordinal)) { spawner(new BoxCollider { Transform = actor.Transform, Size = new Float3(100.0f, 100.0f, 1.0f), }); } else if (modelPath.EndsWith("/Primitives/Capsule.flax", StringComparison.Ordinal)) { var collider = new CapsuleCollider { Transform = actor.Transform, Radius = 25.0f, Height = 50.0f, }; spawner(collider); collider.LocalPosition = new Vector3(0, 50.0f, 0); collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f); } } private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode) { var collider = new BoxCollider { Transform = actor.Transform, }; spawner(collider); // BoxColliderNode fits the box collider automatically on spawn } private void CreateSphere(StaticModel actor, Spawner spawner, bool singleNode) { var bounds = actor.Sphere; var collider = new SphereCollider { Transform = actor.Transform, // Refit into the sphere bounds that are usually calculated from mesh box bounds Position = bounds.Center, Radius = (float)bounds.Radius / Mathf.Max((float)actor.Scale.MaxValue, 0.0001f) * 0.707f, }; spawner(collider); } private void CreateCapsule(StaticModel actor, Spawner spawner, bool singleNode) { var collider = new CapsuleCollider { Transform = actor.Transform, Position = actor.Box.Center, // Size the capsule to best fit the actor Radius = (float)actor.Sphere.Radius / Mathf.Max((float)actor.Scale.MaxValue, 0.0001f) * 0.707f, Height = 100f, }; spawner(collider); } private void CreateConvex(StaticModel actor, Spawner spawner, bool singleNode) { CreateMeshCollider(actor, spawner, singleNode, CollisionDataType.ConvexMesh); } private void CreateTriangle(StaticModel actor, Spawner spawner, bool singleNode) { CreateMeshCollider(actor, spawner, singleNode, CollisionDataType.TriangleMesh); } private void CreateMeshCollider(StaticModel actor, Spawner spawner, bool singleNode, CollisionDataType type) { // Create collision data (or reuse) and add collision actor var created = (CollisionData collisionData) => { var collider = new MeshCollider { Transform = actor.Transform, CollisionData = collisionData, }; spawner(collider); }; var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy(); collisionDataProxy.CreateCollisionDataFromModel(actor.Model, created, singleNode, false, type); } private void OnAddCollider(EditorWindow window, CreateCollider createCollider) { // Allow collider to be added to every static model selection var selection = GetSelection(window).ToArray(); var createdNodes = new List(); foreach (var node in selection) { if (node is not StaticModelNode staticModelNode) continue; var actor = (StaticModel)staticModelNode.Actor; var model = ((StaticModel)staticModelNode.Actor).Model; if (!model) continue; Spawner spawner = collider => { collider.StaticFlags = staticModelNode.Actor.StaticFlags; staticModelNode.Root.Spawn(collider, staticModelNode.Actor); var colliderNode = window is PrefabWindow prefabWindow ? prefabWindow.Graph.Root.Find(collider) : Editor.Instance.Scene.GetActorNode(collider); createdNodes.Add(colliderNode); }; createCollider(actor, spawner, selection.Length == 1); } // Select all created nodes if (window is SceneTreeWindow) { Editor.Instance.SceneEditing.Select(createdNodes); } else if (window is PrefabWindow prefabWindow) { prefabWindow.Select(createdNodes); } } } }