From e95c5731b615756ad7e3306cde98f0d31eaa2ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Tue, 27 Sep 2022 20:23:42 +0200 Subject: [PATCH] Basic Model drag'n'drop --- .../src/EditWindows/ComponentEditWindow.bf | 43 +++ .../src/EditWindows/ContentBrowserWindow.bf | 208 +++++---------- GlitchyEditor/src/EditorContentManager.bf | 247 ++++++++++++++++++ GlitchyEngine/src/Content/ModelLoader.bf | 56 +++- 4 files changed, 415 insertions(+), 139 deletions(-) create mode 100644 GlitchyEditor/src/EditorContentManager.bf diff --git a/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf b/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf index bfb2dd2..c604c5a 100644 --- a/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf +++ b/GlitchyEditor/src/EditWindows/ComponentEditWindow.bf @@ -5,6 +5,7 @@ using GlitchyEngine.Math; using System.Collections; using GlitchyEngine.Renderer; using GlitchyEngine; +using GlitchyEngine.Content; namespace GlitchyEditor.EditWindows { @@ -62,6 +63,7 @@ namespace GlitchyEditor.EditWindows ShowComponentEditor("Sprite Renderer", entity, => ShowSpriteRendererComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Mesh Renderer", entity, => ShowMeshRendererComponentEditor, => ShowComponentContextMenu); ShowComponentEditor("Light", entity, => ShowLightComponentEditor, => ShowComponentContextMenu); + ShowComponentEditor("Mesh", entity, => ShowMeshComponentEditor, => ShowComponentContextMenu); ShowAddComponentButton(entity); } @@ -461,6 +463,47 @@ namespace GlitchyEditor.EditWindows light.Illuminance = illuminance; } + private static void ShowMeshComponentEditor(Entity entity, MeshComponent* meshComponent) + { + ImGui.Button("Drag Mesh here!"); + if (ImGui.BeginDragDropTarget()) + { + ImGui.Payload* payload = ImGui.AcceptDragDropPayload("CONTENT_BROWSER_ITEM"); + + if (payload != null) + { + StringView fullpath = .((char8*)payload.Data, (int)payload.DataSize); + + int idx = fullpath.IndexOf('#'); + + if (idx == -1) + { + // Doesn't make sense here, we NEED a sub asset + Runtime.NotImplemented(); + } + + StringView filePath = fullpath.Substring(0, idx); + StringView meshName = fullpath.Substring(idx + 1); + + // TODO: support multiple primitives (treat every primitive as a single mesh?) + using (GeometryBinding binding = ModelLoader.LoadMesh(filePath, meshName, 0)) + { + meshComponent.Mesh = binding; + } + + //ModelLoader.LoadModel(scope .(path), ) + + /*using (Texture2D newTexture = new Texture2D(path, true)) + { + newTexture.SamplerState = SamplerStateManager.AnisotropicWrap; + material.SetTexture(texture.key, newTexture); + }*/ + } + + ImGui.EndDragDropTarget(); + } + } + private static void ShowAddComponentButton(Entity entity) { static char8[128] searchBuffer = .(); diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index d34b3ce..f4ece72 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -8,70 +8,38 @@ using GlitchyEngine.Math; namespace GlitchyEditor.EditWindows { + using internal GlitchyEditor.EditorContentManager; + class ContentBrowserWindow : EditorWindow { // TODO: Get from project const String ContentDirectory = "./content"; - FileSystemWatcher fsw ~ { - _.StopRaisingEvents(); - delete _; - }; - - private String _currentDirectory ~ delete _; + //private String _currentDirectory ~ delete _; + private TreeNode _currentDirectoryNode; public static SubTexture2D s_FolderTexture; public static SubTexture2D s_FileTexture; + public EditorContentManager _manager ~ delete _; + public this() { - fsw = new FileSystemWatcher(ContentDirectory); - fsw.IncludeSubdirectories = true; - - fsw.OnChanged.Add(new (filename) => { - _fileSystemDirty = true; - _currentDirectoryDirty = true; - }); - - fsw.OnCreated.Add(new (filename) => { - _fileSystemDirty = true; - _currentDirectoryDirty = true; - }); - - fsw.OnDeleted.Add(new (filename) => { - _fileSystemDirty = true; - _currentDirectoryDirty = true; - }); - - fsw.OnRenamed.Add(new (newName, oldName) => { - _fileSystemDirty = true; - _currentDirectoryDirty = true; - }); - - fsw.StartRaisingEvents(); - + _manager = new EditorContentManager(); } protected override void InternalShow() { + _manager.Update(); + + _currentDirectoryNode ??= _manager._assetHierarchy; + if(!ImGui.Begin("Content Browser", &_open, .None)) { ImGui.End(); return; } - if (_fileSystemDirty) - { - BuildDirectoryTree(); - - _fileSystemDirty = false; - } - - if (_currentDirectoryDirty) - { - BuildCurrentDirectory(); - } - ImGui.Columns(2); DrawDirectorySideBar(); @@ -85,101 +53,27 @@ namespace GlitchyEditor.EditWindows ImGui.End(); } - private bool _fileSystemDirty = true; - private bool _currentDirectoryDirty = true; - - class DirectoryNode - { - public String Name ~ delete _; - public String Path ~ delete _; - } - - TreeNode directoryNames = new TreeNode() ~ DeleteTreeAndChildren!(_); - - class Entry - { - public String Name ~ delete _; - public bool IsDirectory; - } - - List _currentDirContent = new .() ~ DeleteContainerAndItems!(_); - - private void BuildDirectoryTree() - { - DeleteTreeAndChildren!(directoryNames); - directoryNames = new TreeNode(); - - String str = scope .(ContentDirectory); - - void AddDirectoryToTree(String path, TreeNode parentNode) - { - DirectoryNode node = new DirectoryNode(); - node.Path = new String(path); - node.Name = new String(); - - Path.GetFileName(node.Path, node.Name); - - var newNode = parentNode.AddChild(node); - - String filter = scope $"{path}/*"; - - for (var directory in Directory.Enumerate(filter, .Directories)) - { - directory.GetFilePath(str..Clear()); - - AddDirectoryToTree(str, newNode); - } - } - - String filter = scope $"{ContentDirectory}/*"; - - for (var directory in Directory.Enumerate(filter, .Directories)) - { - directory.GetFilePath(str..Clear()); - - AddDirectoryToTree(str, directoryNames); - } - } - - private void BuildCurrentDirectory() - { - ClearAndDeleteItems!(_currentDirContent); - - String filter = scope $"{_currentDirectory}/*"; - - String buffer = scope String(); - - for (var entry in Directory.Enumerate(filter, .Directories | .Files)) - { - entry.GetFilePath(buffer..Clear()); - - Entry e = new Entry(); - e.Name = new String(); - Path.GetFileName(buffer, e.Name); - e.IsDirectory = entry.IsDirectory; - - _currentDirContent.Add(e); - } - } - private void DrawDirectorySideBar() { - for(var child in directoryNames.Children) + for(var child in _manager._assetHierarchy.Children) { ImGuiPrintEntityTree(child); } } - private void ImGuiPrintEntityTree(TreeNode tree) + private void ImGuiPrintEntityTree(TreeNode tree) { - String name = tree.Value.Name; + if (!tree->IsDirectory) + return; + + String name = tree->Name; ImGui.TreeNodeFlags flags = .OpenOnArrow | .SpanAvailWidth; if(tree.Children.Count == 0) flags |= .Leaf; - if (tree.Value.Path == _currentDirectory) + if (tree == _currentDirectoryNode) { flags |= .Selected; } @@ -188,10 +82,11 @@ namespace GlitchyEditor.EditWindows if (ImGui.IsItemClicked(.Left)) { - if (_currentDirectory != null) + /*if (_currentDirectory != null) delete _currentDirectory; - _currentDirectory = new String(tree.Value.Path); + _currentDirectory = new String(tree.Value.Path);*/ + _currentDirectoryNode = tree; } if(isOpen) @@ -205,35 +100,38 @@ namespace GlitchyEditor.EditWindows } } - private static Vector2 DirectoryItemSize = .(100, 100); + private static Vector2 DirectoryItemSize = .(110, 110); const Vector2 padding = .(24, 24); private void DrawCurrentDirectory() { + if (_currentDirectoryNode == null) + return; + ImGui.Style* style = ImGui.GetStyle(); float window_visible_x2 = ImGui.GetWindowPos().x + ImGui.GetWindowContentRegionMax().x; - for (var entry in _currentDirContent) + for (var entry in _currentDirectoryNode.Children) { - ImGui.PushID(entry.Name); + ImGui.PushID(entry->Name); DrawDirectoryItem(entry); float last_button_x2 = ImGui.GetItemRectMax().x; float next_button_x2 = last_button_x2 + style.ItemSpacing.x + DirectoryItemSize.X; // Expected position if next button was on same line - if (entry != _currentDirContent.Back && next_button_x2 < window_visible_x2) + if (entry != _currentDirectoryNode.Children.Back && next_button_x2 < window_visible_x2) ImGui.SameLine(); ImGui.PopID(); } } - private void DrawDirectoryItem(Entry entry) + private void DrawDirectoryItem(TreeNode entry) { ImGui.BeginChild("item", (.)DirectoryItemSize); - SubTexture2D image = entry.IsDirectory ? s_FolderTexture : s_FileTexture; + SubTexture2D image = entry->IsDirectory ? s_FolderTexture : s_FileTexture; ImGui.PushStyleColor(.Button, ImGui.Vec4(0, 0, 0, 0)); @@ -241,7 +139,7 @@ namespace GlitchyEditor.EditWindows if (ImGui.BeginDragDropSource()) { - String fullpath = scope $"{_currentDirectory}{Path.DirectorySeparatorChar}{entry.Name}"; + String fullpath = scope $"{_currentDirectoryNode->Path}{Path.DirectorySeparatorChar}{entry->Name}"; // TODO: this is dirty if (fullpath.StartsWith(ContentDirectory, .OrdinalIgnoreCase)) @@ -259,17 +157,51 @@ namespace GlitchyEditor.EditWindows ImGui.PopStyleColor(); - ImGui.TextUnformatted(entry.Name); + ImGui.TextUnformatted(entry->Name); + + if (entry->SubAssets?.Count > 0) + { + // Button for revealing sub assets (e.g. Meshes in 3D-Model) + + ImGui.SameLine(); + if (ImGui.Button(">")) + ImGui.OpenPopup("SubAssets"); + } + + if (ImGui.BeginPopup("SubAssets", .Popup)) + { + for (var subAsset in entry->SubAssets) + { + ImGui.Button(subAsset.Name); + + if (ImGui.BeginDragDropSource()) + { + String fullpath = Path.InternalCombine(.. scope String(), _currentDirectoryNode->Path, entry->Name); + fullpath.AppendF($"#{subAsset.Name}"); + //scope $"{_currentDirectoryNode->Path}{Path.DirectorySeparatorChar}{entry->Name}"; + //String fullpath = scope $"{subAsset.Asset.Path}#{subAsset.Name}"; + + // TODO: this is dirty + //if (fullpath.StartsWith(ContentDirectory, .OrdinalIgnoreCase)) + // fullpath.Remove(0, ContentDirectory.Length); + + ImGui.SetDragDropPayload("CONTENT_BROWSER_ITEM", fullpath.CStr(), (.)fullpath.Length, .Once); + + ImGui.EndDragDropSource(); + } + } + + ImGui.EndPopup(); + } ImGui.EndChild(); } - private void EntryDoubleClicked(Entry entry) + private void EntryDoubleClicked(TreeNode entry) { - if (entry.IsDirectory) + if (entry->IsDirectory) { - _currentDirectory.Append(Path.DirectorySeparatorChar); - _currentDirectory.Append(entry.Name); + _currentDirectoryNode = entry; } } } diff --git a/GlitchyEditor/src/EditorContentManager.bf b/GlitchyEditor/src/EditorContentManager.bf new file mode 100644 index 0000000..b41ac54 --- /dev/null +++ b/GlitchyEditor/src/EditorContentManager.bf @@ -0,0 +1,247 @@ +using System; +using System.IO; +using GlitchyEngine.Collections; +using System.Collections; +using GlitchyEngine.Renderer; +using System.Threading; +using GlitchyEngine.Content; + +namespace GlitchyEditor; + +/*class PreviewImageManager +{ + /// Thread that loads/generates preview images in the background + //private Thread _loaderThread; + + /// Set to true to notify the loader to stop + //private bool _stopLoader; + + private append Dictionary _previewImages = .() ~ { + for (var v in _) + { + delete v.key; + v.value.ReleaseRef(); + } + + delete _; + }; + + public Texture2D GetPreviewImage(String assetName) + { + if (_previewImages.TryGetValue(assetName, let image)) + { + return image..AddRef(); + } + + if (assetName.EndsWith(".png", .OrdinalIgnoreCase)) + { + /*using (Texture2D texture = new Texture2D(assetName, true)) + { + Texture2DDesc desc = .(128, 128, .R8G8B8A8_UNorm_SRGB) + { + Usage = .Immutable + }; + + Texture2D myActualTexture = new Texture2D(desc); + + // TODO: Scale down texture (on GPU?) + }*/ + + Texture2D texture = new Texture2D(assetName, true); + + if (texture.Width <= 128 && texture.Height <= 128) + { + + } + + if ( texture.MipLevels > 1) + { + + } + + _previewImages.Add(new String(assetName), texture); + + return texture..AddRef(); + } + else + { + Runtime.NotImplemented(); + } + } +}*/ + +class EditorContentManager +{ + // TODO: Get from project + const String ContentDirectory = "./content"; + + FileSystemWatcher fsw ~ { + _.StopRaisingEvents(); + delete _; + }; + + bool _fileSystemDirty = true; + + public class AssetNode + { + public String Name ~ delete _; + public String Path ~ delete _; + + public bool IsDirectory; + + public List SubAssets ~ { + SubAssets?.ClearAndDeleteItems(); + delete SubAssets; + } + + public Texture2D PreviewImage ~ _?.ReleaseRef(); + } + + public class Asset + { + public AssetNode Asset; + public String Name ~ delete _; + //public String AssetInternalPath ~ delete _; + + public Texture2D PreviewImage ~ _?.ReleaseRef(); + } + + internal TreeNode _assetHierarchy = new TreeNode() ~ DeleteTreeAndChildren!(_); + + public this() + { + SetupFileSystemWatcher(); + } + + /// Initializes the FSW for the current ContentDirectory and registers the events. + private void SetupFileSystemWatcher() + { + fsw = new FileSystemWatcher(ContentDirectory); + fsw.IncludeSubdirectories = true; + + fsw.OnChanged.Add(new (filename) => { + _fileSystemDirty = true; + }); + + fsw.OnCreated.Add(new (filename) => { + _fileSystemDirty = true; + }); + + fsw.OnDeleted.Add(new (filename) => { + _fileSystemDirty = true; + }); + + fsw.OnRenamed.Add(new (newName, oldName) => { + _fileSystemDirty = true; + }); + + fsw.StartRaisingEvents(); + } + + public void Update() + { + if (_fileSystemDirty) + { + UpdateFiles(); + } + } + + private void UpdateFiles() + { + DeleteTreeAndChildren!(_assetHierarchy); + _assetHierarchy = new TreeNode(new AssetNode()); + _assetHierarchy->Path = new String(ContentDirectory); + _assetHierarchy->Name = new String("Content"); + + String str = scope .(ContentDirectory); + + void GrabSubAssets(TreeNode assetNode) + { + // TODO: Dedicated asset loaders that register their extensions, etc....! + + if (assetNode->Name.EndsWith(".png", .OrdinalIgnoreCase)) + { + assetNode->SubAssets = new List(); + assetNode->SubAssets.Add(new Asset() + { + Asset = assetNode.Value, + Name = new String(assetNode->Name) + }); + } + else if (assetNode->Name.EndsWith(".gltf", .OrdinalIgnoreCase) || assetNode->Name.EndsWith(".glb", .OrdinalIgnoreCase)) + { + List meshNames = scope List(); + ModelLoader.GetMeshNames(assetNode->Path, meshNames); + + assetNode->SubAssets = new List(); + + for (var meshName in meshNames) + { + assetNode->SubAssets.Add(new Asset() + { + Asset = assetNode.Value, + // Pass ownership of meshName to SubAsset -> save a copy and delete + Name = meshName + }); + } + } + } + + void AddFilesOfDirectory(TreeNode directory) + { + String filter = scope $"{directory->Path}/*"; + + String buffer = scope String(); + + for (var entry in Directory.Enumerate(filter, .Files)) + { + entry.GetFilePath(buffer..Clear()); + + AssetNode e = new AssetNode(); + e.Name = new String(); + Path.GetFileName(buffer, e.Name); + + e.Path = new String(buffer); + + e.IsDirectory = entry.IsDirectory; + + var node = directory.AddChild(e); + GrabSubAssets(node); + } + } + + void AddDirectoryToTree(String path, TreeNode parentNode) + { + AssetNode node = new AssetNode(); + node.Path = new String(path); + node.Name = new String(); + node.IsDirectory = true; + + Path.GetFileName(node.Path, node.Name); + + var newNode = parentNode.AddChild(node); + + String filter = scope $"{path}/*"; + + for (var directory in Directory.Enumerate(filter, .Directories)) + { + directory.GetFilePath(str..Clear()); + + AddDirectoryToTree(str, newNode); + } + + AddFilesOfDirectory(newNode); + } + + String filter = scope $"{ContentDirectory}/*"; + + for (var directory in Directory.Enumerate(filter, .Directories)) + { + directory.GetFilePath(str..Clear()); + + AddDirectoryToTree(str, _assetHierarchy); + } + + _fileSystemDirty = false; + } +} \ No newline at end of file diff --git a/GlitchyEngine/src/Content/ModelLoader.bf b/GlitchyEngine/src/Content/ModelLoader.bf index 6244e10..3e9933b 100644 --- a/GlitchyEngine/src/Content/ModelLoader.bf +++ b/GlitchyEngine/src/Content/ModelLoader.bf @@ -12,13 +12,67 @@ namespace GlitchyEngine.Content { static readonly Matrix RightToLeftHand = .Scaling(1, 1, -1); + public static Result GetMeshNames(String filename, List meshNames) + { + CGLTF.Options options = .(); + CGLTF.Data* data; + CGLTF.Result result = CGLTF.ParseFile(options, filename, out data); + + if (!(result case .Success)) + return .Err; + + for (var mesh in data.Meshes) + { + meshNames.Add(new String(mesh.Name)); + } + + CGLTF.Free(data); + + return .Ok; + } + + public static GeometryBinding LoadMesh(StringView fileName, StringView meshName, int primitiveIndex) + { + // TODO: add a context to remember which buffers were loaded before so that we don't load the same data multiple times. + + char8* scopedFileName = fileName.ToScopeCStr!(); + + CGLTF.Options options = .(); + CGLTF.Data* data; + CGLTF.Result result = CGLTF.ParseFile(options, scopedFileName, out data); + + if (!(result case .Success)) + return null; + + result = CGLTF.LoadBuffers(options, data, scopedFileName); + + GeometryBinding geoBinding = null; + + for (var mesh in data.Meshes) + { + var name = StringView(mesh.Name); + + if (name == meshName) + { + Log.EngineLogger.AssertDebug(primitiveIndex >= 0 && primitiveIndex < mesh.Primitives.Length); + + geoBinding = PrimitiveToGeoBinding(mesh.Primitives[primitiveIndex]); + break; + } + } + + CGLTF.Free(data); + + return geoBinding; + } + public static EcsEntity LoadModel(String filename, Material material, EcsWorld world, List outClips, StringView entityName = StringView()) { CGLTF.Options options = .(); CGLTF.Data* data; CGLTF.Result result = CGLTF.ParseFile(options, filename, out data); - + Log.EngineLogger.Assert(result == .Success, "Failed to load model."); result = CGLTF.LoadBuffers(options, data, filename);