diff --git a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs new file mode 100644 index 000000000..71c5f6daf --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs @@ -0,0 +1,336 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.CustomEditors.Elements; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + /// + [CustomEditor(typeof(ModelInstanceActor.MeshReference)), DefaultEditor] + public class MeshReferenceEditor : CustomEditor + { + private class MeshRefPickerControl : Control + { + private ModelInstanceActor.MeshReference _value = new ModelInstanceActor.MeshReference { LODIndex = -1, MeshIndex = -1 }; + private string _valueName; + private Float2 _mousePos; + + public string[][] MeshNames; + public event Action ValueChanged; + + public ModelInstanceActor.MeshReference Value + { + get => _value; + set + { + if (_value.LODIndex == value.LODIndex && _value.MeshIndex == value.MeshIndex) + return; + _value = value; + if (value.LODIndex == -1 || value.MeshIndex == -1) + _valueName = null; + else if (MeshNames.Length == 1) + _valueName = MeshNames[value.LODIndex][value.MeshIndex]; + else + _valueName = $"LOD{value.LODIndex} - {MeshNames[value.LODIndex][value.MeshIndex]}"; + ValueChanged?.Invoke(); + } + } + + public MeshRefPickerControl() + : base(0, 0, 50, 16) + { + } + + private void ShowDropDownMenu() + { + // Show context menu with tree structure of LODs and meshes + Focus(); + var cm = new ItemsListContextMenu(200); + var meshNames = MeshNames; + var actor = _value.Actor; + for (int lodIndex = 0; lodIndex < meshNames.Length; lodIndex++) + { + var item = new ItemsListContextMenu.Item + { + Name = "LOD" + lodIndex, + Tag = new ModelInstanceActor.MeshReference { Actor = actor, LODIndex = lodIndex, MeshIndex = 0 }, + TintColor = new Color(0.8f, 0.8f, 1.0f, 0.8f), + }; + cm.AddItem(item); + + for (int meshIndex = 0; meshIndex < meshNames[lodIndex].Length; meshIndex++) + { + item = new ItemsListContextMenu.Item + { + Name = " " + meshNames[lodIndex][meshIndex], + Tag = new ModelInstanceActor.MeshReference { Actor = actor, LODIndex = lodIndex, MeshIndex = meshIndex }, + }; + if (_value.LODIndex == lodIndex && _value.MeshIndex == meshIndex) + item.BackgroundColor = FlaxEngine.GUI.Style.Current.BackgroundSelected; + cm.AddItem(item); + } + } + cm.ItemClicked += item => Value = (ModelInstanceActor.MeshReference)item.Tag; + cm.Show(Parent, BottomLeft); + } + + /// + public override void Draw() + { + base.Draw(); + + // Cache data + var style = FlaxEngine.GUI.Style.Current; + bool isSelected = _valueName != null; + bool isEnabled = EnabledInHierarchy; + var frameRect = new Rectangle(0, 0, Width, 16); + if (isSelected) + frameRect.Width -= 16; + frameRect.Width -= 16; + var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14); + var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14); + var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14); + + // Draw frame + Render2D.DrawRectangle(frameRect, isEnabled && (IsMouseOver || IsNavFocused) ? style.BorderHighlighted : style.BorderNormal); + + // Check if has item selected + if (isSelected) + { + // Draw name + Render2D.PushClip(nameRect); + Render2D.DrawText(style.FontMedium, _valueName, nameRect, isEnabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); + Render2D.PopClip(); + + // Draw deselect button + Render2D.DrawSprite(style.Cross, button1Rect, isEnabled && button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); + } + else + { + // Draw info + Render2D.DrawText(style.FontMedium, "-", nameRect, isEnabled ? Color.OrangeRed : Color.DarkOrange, TextAlignment.Near, TextAlignment.Center); + } + + // Draw picker button + var pickerRect = isSelected ? button2Rect : button1Rect; + Render2D.DrawSprite(style.ArrowDown, pickerRect, isEnabled && pickerRect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); + } + + /// + public override void OnMouseEnter(Float2 location) + { + _mousePos = location; + + base.OnMouseEnter(location); + } + + /// + public override void OnMouseLeave() + { + _mousePos = Float2.Minimum; + + base.OnMouseLeave(); + } + + /// + public override void OnMouseMove(Float2 location) + { + _mousePos = location; + + base.OnMouseMove(location); + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + // Cache data + bool isSelected = _valueName != null; + var frameRect = new Rectangle(0, 0, Width, 16); + if (isSelected) + frameRect.Width -= 16; + frameRect.Width -= 16; + var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14); + var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14); + var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14); + + // Deselect + if (isSelected && button1Rect.Contains(ref location)) + Value = new ModelInstanceActor.MeshReference { Actor = null, LODIndex = -1, MeshIndex = -1 }; + + // Picker dropdown menu + if ((isSelected ? button2Rect : button1Rect).Contains(ref location)) + ShowDropDownMenu(); + + return base.OnMouseUp(location, button); + } + + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + Focus(); + + // Open model editor window + if (_value.Actor is StaticModel staticModel) + Editor.Instance.ContentEditing.Open(staticModel.Model); + else if (_value.Actor is AnimatedModel animatedModel) + Editor.Instance.ContentEditing.Open(animatedModel.SkinnedModel); + + return base.OnMouseDoubleClick(location, button); + } + + /// + public override void OnSubmit() + { + base.OnSubmit(); + + ShowDropDownMenu(); + } + + /// + public override void OnDestroy() + { + MeshNames = null; + _valueName = null; + + base.OnDestroy(); + } + } + + private ModelInstanceActor _actor; + private CustomElement _actorPicker; + private CustomElement _meshPicker; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + // Get the context actor to pick the mesh from it + if (GetActor(out var actor)) + { + // TODO: support editing multiple values + layout.Label("Different values"); + return; + } + _actor = actor; + + var showActorPicker = actor == null || ParentEditor.Values.All(x => x is not Cloth); + if (showActorPicker) + { + // Actor reference picker + _actorPicker = layout.Custom(); + _actorPicker.CustomControl.Type = new ScriptType(typeof(ModelInstanceActor)); + _actorPicker.CustomControl.ValueChanged += () => SetValue(new ModelInstanceActor.MeshReference { Actor = (ModelInstanceActor)_actorPicker.CustomControl.Value }); + } + + if (actor != null) + { + // Get mesh names hierarchy + string[][] meshNames; + if (actor is StaticModel staticModel) + { + var model = staticModel.Model; + if (model == null || model.WaitForLoaded()) + return; + var materials = model.MaterialSlots; + var lods = model.LODs; + meshNames = new string[lods.Length][]; + for (int lodIndex = 0; lodIndex < lods.Length; lodIndex++) + { + var lodMeshes = lods[lodIndex].Meshes; + meshNames[lodIndex] = new string[lodMeshes.Length]; + for (int meshIndex = 0; meshIndex < lodMeshes.Length; meshIndex++) + { + var mesh = lodMeshes[meshIndex]; + var materialName = materials[mesh.MaterialSlotIndex].Name; + if (string.IsNullOrEmpty(materialName) && materials[mesh.MaterialSlotIndex].Material) + materialName = Path.GetFileNameWithoutExtension(materials[mesh.MaterialSlotIndex].Material.Path); + if (string.IsNullOrEmpty(materialName)) + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex}"; + else + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex} ({materialName})"; + } + } + } + else if (actor is AnimatedModel animatedModel) + { + var skinnedModel = animatedModel.SkinnedModel; + if (skinnedModel == null || skinnedModel.WaitForLoaded()) + return; + var materials = skinnedModel.MaterialSlots; + var lods = skinnedModel.LODs; + meshNames = new string[lods.Length][]; + for (int lodIndex = 0; lodIndex < lods.Length; lodIndex++) + { + var lodMeshes = lods[lodIndex].Meshes; + meshNames[lodIndex] = new string[lodMeshes.Length]; + for (int meshIndex = 0; meshIndex < lodMeshes.Length; meshIndex++) + { + var mesh = lodMeshes[meshIndex]; + var materialName = materials[mesh.MaterialSlotIndex].Name; + if (string.IsNullOrEmpty(materialName) && materials[mesh.MaterialSlotIndex].Material) + materialName = Path.GetFileNameWithoutExtension(materials[mesh.MaterialSlotIndex].Material.Path); + if (string.IsNullOrEmpty(materialName)) + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex}"; + else + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex} ({materialName})"; + } + } + } + else + return; // Not supported model type + + // Mesh reference picker + _meshPicker = layout.Custom(); + _meshPicker.CustomControl.MeshNames = meshNames; + _meshPicker.CustomControl.Value = (ModelInstanceActor.MeshReference)Values[0]; + _meshPicker.CustomControl.ValueChanged += () => SetValue(_meshPicker.CustomControl.Value); + } + } + + /// + public override void Refresh() + { + base.Refresh(); + + if (_actorPicker != null) + { + GetActor(out var actor); + _actorPicker.CustomControl.Value = actor; + if (actor != _actor) + { + RebuildLayout(); + return; + } + } + if (_meshPicker != null) + { + _meshPicker.CustomControl.Value = (ModelInstanceActor.MeshReference)Values[0]; + } + } + + private bool GetActor(out ModelInstanceActor actor) + { + actor = null; + foreach (ModelInstanceActor.MeshReference value in Values) + { + if (actor == null) + actor = value.Actor; + else if (actor != value.Actor) + return true; + } + return false; + } + } +} diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 882026db7..5f2d65917 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -945,6 +945,18 @@ bool AnimatedModel::IntersectsEntry(const Ray& ray, Real& distance, Vector3& nor return result; } +bool AnimatedModel::GetMeshData(const MeshReference& mesh, MeshBufferType type, BytesContainer& result, int32& count) const +{ + count = 0; + if (mesh.LODIndex < 0 || mesh.MeshIndex < 0) + return true; + const auto model = SkinnedModel.Get(); + if (!model || model->WaitForLoaded()) + return true; + auto& lod = model->LODs[Math::Min(mesh.LODIndex, model->LODs.Count() - 1)]; + return lod.Meshes[Math::Min(mesh.MeshIndex, lod.Meshes.Count() - 1)].DownloadDataCPU(type, result, count); +} + void AnimatedModel::OnDeleteObject() { // Ensure this object is no longer referenced for anim update diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index ccd0ee3b5..904216356 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -374,6 +374,7 @@ public: void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; + bool GetMeshData(const MeshReference& mesh, MeshBufferType type, BytesContainer& result, int32& count) const override; void OnDeleteObject() override; protected: diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.h b/Source/Engine/Level/Actors/ModelInstanceActor.h index 825b64c23..850bc8715 100644 --- a/Source/Engine/Level/Actors/ModelInstanceActor.h +++ b/Source/Engine/Level/Actors/ModelInstanceActor.h @@ -12,6 +12,23 @@ API_CLASS(Abstract) class FLAXENGINE_API ModelInstanceActor : public Actor { DECLARE_SCENE_OBJECT_ABSTRACT(ModelInstanceActor); + + /// + /// Utility container to reference a single mesh within . + /// + API_STRUCT(NoDefault) struct MeshReference : ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(MeshReference); + API_AUTO_SERIALIZATION(); + + // Owning actor. + API_FIELD() ScriptingObjectReference Actor; + // Index of the LOD (Level Of Detail). + API_FIELD() int32 LODIndex = 0; + // Index of the mesh (within the LOD). + API_FIELD() int32 MeshIndex = 0; + }; + protected: int32 _sceneRenderingKey = -1; // Uses SceneRendering::DrawCategory::SceneDrawAsync @@ -24,8 +41,8 @@ public: /// /// Gets the model entries collection. Each entry contains data how to render meshes using this entry (transformation, material, shadows casting, etc.). /// - API_PROPERTY(Attributes="Serialize, EditorOrder(1000), EditorDisplay(\"Entries\", EditorDisplayAttribute.InlineStyle), Collection(CanReorderItems = false, NotNullItems = true, ReadOnly = true, Spacing = 10)") - FORCE_INLINE Array GetEntries() const + API_PROPERTY(Attributes="Serialize, EditorOrder(1000), EditorDisplay(\"Entries\", EditorDisplayAttribute.InlineStyle), Collection(CanReorderItems=false, NotNullItems=true, ReadOnly=true, Spacing=10)") + FORCE_INLINE const Array& GetEntries() const { return Entries; } @@ -80,6 +97,19 @@ public: { return false; } + + /// + /// Extracts mesh buffer data from CPU. Might be cached internally (eg. by Model/SkinnedModel). + /// + /// Mesh reference. + /// Buffer type + /// The result data + /// The amount of items inside the result buffer. + /// True if failed, otherwise false + virtual bool GetMeshData(const MeshReference& mesh, MeshBufferType type, BytesContainer& result, int32& count) const + { + return true; + } protected: virtual void WaitForModelLoad(); diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index 3eb3b6012..4e67d90d2 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -588,6 +588,18 @@ bool StaticModel::IntersectsEntry(const Ray& ray, Real& distance, Vector3& norma return result; } +bool StaticModel::GetMeshData(const MeshReference& mesh, MeshBufferType type, BytesContainer& result, int32& count) const +{ + count = 0; + if (mesh.LODIndex < 0 || mesh.MeshIndex < 0) + return true; + const auto model = Model.Get(); + if (!model || model->WaitForLoaded()) + return true; + auto& lod = model->LODs[Math::Min(mesh.LODIndex, model->LODs.Count() - 1)]; + return lod.Meshes[Math::Min(mesh.MeshIndex, lod.Meshes.Count() - 1)].DownloadDataCPU(type, result, count); +} + void StaticModel::OnTransformChanged() { // Base diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index af3aa53c0..ffb34e0a1 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -168,6 +168,7 @@ public: void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; + bool GetMeshData(const MeshReference& mesh, MeshBufferType type, BytesContainer& result, int32& count) const override; protected: // [ModelInstanceActor]