// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Xml; using FlaxEditor.Content; using FlaxEditor.Content.Create; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Previews; using FlaxEngine; using FlaxEngine.GUI; using Object = FlaxEngine.Object; namespace FlaxEditor.Windows.Assets { /// /// Editor window to view/modify asset. /// /// /// public sealed class CollisionDataWindow : AssetEditorWindowBase { [Flags] private enum MaterialSlotsMask : uint { // @formatter:off Slot0=1u<<0,Slot1=1u<<1,Slot2=1u<<2,Slot3=1u<<3,Slot4=1u<<4,Slot5=1u<<5,Slot6=1u<<6,Slot7=1u<<7,Slot8=1u<<8,Slot9=1u<<9,Slot10=1u<<10,Slot11=1u<<11,Slot12=1u<<12,Slot13=1u<<13,Slot14=1u<<14,Slot15=1u<<15,Slot16=1u<<16,Slot17=1u<<17,Slot18=1u<<18,Slot19=1u<<19,Slot20=1u<<20,Slot21=1u<<21,Slot22=1u<<22,Slot23=1u<<23,Slot24=1u<<24,Slot25=1u<<25,Slot26=1u<<26,Slot27=1u<<27,Slot28=1u<<28,Slot29=1u<<29,Slot30=1u<<30,Slot31=1u<<31, // @formatter:on All = uint.MaxValue, } /// /// The asset properties proxy object. /// [CustomEditor(typeof(Editor))] private sealed class PropertiesProxy { private CollisionDataWindow Window; private CollisionData Asset; private bool _isCooking; [EditorOrder(0), EditorDisplay("General"), Tooltip("Type of the collision data to use")] public CollisionDataType Type; [EditorOrder(10), EditorDisplay("General"), Tooltip("Source model asset to use for collision data generation")] public ModelBase Model; [EditorOrder(20), Limit(0, 5), EditorDisplay("General", "Model LOD Index"), Tooltip("Source model LOD index to use for collision data generation (will be clamped to the actual model LODs collection size)")] public int ModelLodIndex; [EditorOrder(30), EditorDisplay("General"), Tooltip("The source model material slots mask. One bit per-slot. Can be used to exclude particular material slots from collision cooking.")] public MaterialSlotsMask MaterialSlotsMask = MaterialSlotsMask.All; [EditorOrder(100), EditorDisplay("Convex Mesh", "Convex Flags"), Tooltip("Convex mesh generation flags")] public ConvexMeshGenerationFlags ConvexFlags; [EditorOrder(110), Limit(8, 255), EditorDisplay("Convex Mesh", "Vertex Limit"), Tooltip("Convex mesh vertex count limit")] public int ConvexVertexLimit; public class Editor : GenericEditor { private ButtonElement _cookButton; /// public override void Initialize(LayoutElementsContainer layout) { base.Initialize(layout); layout.Space(10); _cookButton = layout.Button("Cook"); _cookButton.Button.Clicked += OnCookButtonClicked; } /// public override void Refresh() { if (_cookButton != null && Values.Count == 1) { var p = (PropertiesProxy)Values[0]; if (p._isCooking) { _cookButton.Button.Enabled = false; _cookButton.Button.Text = "Cooking..."; } else { _cookButton.Button.Enabled = p.Type != CollisionDataType.None && p.Model != null; _cookButton.Button.Text = "Cook"; } } base.Refresh(); } private void OnCookButtonClicked() { ((PropertiesProxy)Values[0]).Cook(); } } private class CookData : CreateFileEntry { public PropertiesProxy Proxy; public CollisionDataType Type; public ModelBase Model; public int ModelLodIndex; public uint MaterialSlotsMask; public ConvexMeshGenerationFlags ConvexFlags; public int ConvexVertexLimit; public CookData(string resultUrl) : base("Collision Data", resultUrl) { } /// public override bool Create() { bool failed = FlaxEditor.Editor.CookMeshCollision(ResultUrl, Type, Model, ModelLodIndex, MaterialSlotsMask, ConvexFlags, ConvexVertexLimit); Proxy._isCooking = false; Proxy.Window.UpdateWiresModel(); return failed; } } public void Cook() { _isCooking = true; Window.Editor.ContentImporting.Create(new CookData(Asset.Path) { Proxy = this, Type = Type, Model = Model, ModelLodIndex = ModelLodIndex, MaterialSlotsMask = (uint)MaterialSlotsMask, ConvexFlags = ConvexFlags, ConvexVertexLimit = ConvexVertexLimit, }); } public void OnLoad(CollisionDataWindow window) { // Link Window = window; Asset = window.Asset; // Setup cooking parameters var options = Asset.Options; Type = options.Type; if (Type == CollisionDataType.None) Type = CollisionDataType.ConvexMesh; Model = FlaxEngine.Content.LoadAsync(options.Model); ModelLodIndex = options.ModelLodIndex; ConvexFlags = options.ConvexFlags; ConvexVertexLimit = options.ConvexVertexLimit; } public void OnClean() { // Unlink Window = null; Asset = null; Model = null; } } private readonly SplitPanel _split; private readonly ModelBasePreview _preview; private readonly CustomEditorPresenter _propertiesPresenter; private readonly PropertiesProxy _properties; private Model _collisionWiresModel; private StaticModel _collisionWiresShowActor; private bool _updateWireMesh; /// public CollisionDataWindow(Editor editor, AssetItem item) : base(editor, item) { // Toolstrip _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.CenterView64, () => _preview.ResetCamera()).LinkTooltip("Show whole collision"); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/physics/colliders/collision-data.html")).LinkTooltip("See documentation to learn more"); // Split Panel _split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolstrip.Bottom, 0), SplitterValue = 0.7f, Parent = this }; // Model preview _preview = new ModelBasePreview(true) { ViewportCamera = new FPSCamera(), Parent = _split.Panel1 }; _preview.Task.ViewFlags &= ~ViewFlags.Sky & ~ViewFlags.Bloom; // Asset properties _propertiesPresenter = new CustomEditorPresenter(null); _propertiesPresenter.Panel.Parent = _split.Panel2; _properties = new PropertiesProxy(); _propertiesPresenter.Select(_properties); } /// public override void Update(float deltaTime) { // Sync helper actor size with actual preview model (preview scales model for better usage experience) if (_collisionWiresShowActor && _collisionWiresShowActor.IsActive) { _collisionWiresShowActor.Transform = _preview.StaticModel.Transform; } base.Update(deltaTime); } /// /// Updates the collision data debug model. /// private void UpdateWiresModel() { // Don't update on a importer/worker thread if (Platform.CurrentThreadID != Globals.MainThreadID) { _updateWireMesh = true; return; } if (_collisionWiresModel == null) { _collisionWiresModel = FlaxEngine.Content.CreateVirtualAsset(); _collisionWiresModel.SetupLODs(new[] { 1 }); } Editor.Internal_GetCollisionWires(FlaxEngine.Object.GetUnmanagedPtr(Asset), out var triangles, out var indices, out var _, out var _); if (triangles != null && indices != null) _collisionWiresModel.LODs[0].Meshes[0].UpdateMesh(triangles, indices); else Editor.LogWarning("Failed to get collision wires for " + Asset); if (_collisionWiresShowActor == null) { _collisionWiresShowActor = new StaticModel(); _preview.Task.AddCustomActor(_collisionWiresShowActor); } _collisionWiresShowActor.Model = _collisionWiresModel; _collisionWiresShowActor.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WiresDebugMaterial)); _preview.Asset = FlaxEngine.Content.LoadAsync(_asset.Options.Model); } /// protected override void UnlinkItem() { _properties.OnClean(); _preview.Asset = null; base.UnlinkItem(); } /// protected override void OnAssetLinked() { _preview.Asset = null; base.OnAssetLinked(); } /// protected override void OnAssetLoaded() { _properties.OnLoad(this); _propertiesPresenter.BuildLayout(); ClearEditedFlag(); UpdateWiresModel(); base.OnAssetLoaded(); } /// public override void OnItemReimported(ContentItem item) { // Refresh the properties (will get new data in OnAssetLoaded) _properties.OnClean(); _propertiesPresenter.BuildLayout(); ClearEditedFlag(); base.OnItemReimported(item); } /// public override void OnUpdate() { if (_updateWireMesh) { _updateWireMesh = false; UpdateWiresModel(); } base.OnUpdate(); } /// public override void OnDestroy() { base.OnDestroy(); Object.Destroy(ref _collisionWiresShowActor); Object.Destroy(ref _collisionWiresModel); } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { LayoutSerializeSplitter(writer, "Split", _split); } /// public override void OnLayoutDeserialize(XmlElement node) { LayoutDeserializeSplitter(node, "Split", _split); } /// public override void OnLayoutDeserialize() { _split.SplitterValue = 0.7f; } } }