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]