diff --git a/Core/Source/Lux/Core/Events/SceneEvents.h b/Core/Source/Lux/Core/Events/SceneEvents.h new file mode 100644 index 0000000..5bc52d4 --- /dev/null +++ b/Core/Source/Lux/Core/Events/SceneEvents.h @@ -0,0 +1,122 @@ +#pragma once + +#include "Event.h" +#include "Lux/Scene/Scene.h" +#include "Lux/Editor/SelectionManager.h" + +#include + +namespace Lux { + + class SceneEvent : public Event + { + public: + const Ref& GetScene() const { return m_Scene; } + Ref GetScene() { return m_Scene; } + + EVENT_CLASS_CATEGORY(EventCategoryApplication | EventCategoryScene) + protected: + SceneEvent(const Ref& scene) + : m_Scene(scene) { + } + + Ref m_Scene; + }; + + class ScenePreStartEvent : public SceneEvent + { + public: + ScenePreStartEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePreStartEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePreStart) + }; + + class ScenePostStartEvent : public SceneEvent + { + public: + ScenePostStartEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePostStartEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePostStart) + }; + + class ScenePreStopEvent : public SceneEvent + { + public: + ScenePreStopEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePreStopEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePreStop) + }; + + class ScenePostStopEvent : public SceneEvent + { + public: + ScenePostStopEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePostStopEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePostStop) + }; + + // TODO(Peter): Probably move this somewhere else... + class SelectionChangedEvent : public Event + { + public: + SelectionChangedEvent(SelectionContext contextID, UUID selectionID, bool selected) + : m_Context(contextID), m_SelectionID(selectionID), m_Selected(selected) + { + } + + SelectionContext GetContextID() const { return m_Context; } + UUID GetSelectionID() const { return m_SelectionID; } + bool IsSelected() const { return m_Selected; } + + std::string ToString() const override + { + std::stringstream ss; + ss << "EntitySelectionChangedEvent: Context(" << (int32_t)m_Context << "), Selection(" << m_SelectionID << "), " << m_Selected; + return ss.str(); + } + + EVENT_CLASS_CATEGORY(EventCategoryScene) + EVENT_CLASS_TYPE(SelectionChanged) + private: + SelectionContext m_Context; + UUID m_SelectionID; + bool m_Selected; + }; + +} diff --git a/Core/Source/Lux/Core/Log.cpp b/Core/Source/Lux/Core/Log.cpp index d522390..5fbfe92 100644 --- a/Core/Source/Lux/Core/Log.cpp +++ b/Core/Source/Lux/Core/Log.cpp @@ -3,7 +3,7 @@ #include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/sinks/basic_file_sink.h" -//#include "Lux/Editor/EditorConsole/EditorConsoleSink.h" +#include "Lux/Editor/EditorConsole/EditorConsoleSink.h" #include @@ -58,34 +58,34 @@ namespace Lux { std::make_shared() #endif }; - /* + std::vector editorConsoleSinks = { std::make_shared("logs/APP.log", true), #if LUX_HAS_CONSOLE - std::make_shared(1), + //std::make_shared(1), std::make_shared() #endif - };*/ + }; luxSinks[0]->set_pattern("[%T] [%l] %n: %v"); appSinks[0]->set_pattern("[%T] [%l] %n: %v"); -/* + #if LUX_HAS_CONSOLE luxSinks[1]->set_pattern("%^[%T] %n: %v%$"); appSinks[1]->set_pattern("%^[%T] %n: %v%$"); for (auto sink : editorConsoleSinks) sink->set_pattern("%^%v%$"); -#endif*/ +#endif s_CoreLogger = std::make_shared("LUX", luxSinks.begin(), luxSinks.end()); s_CoreLogger->set_level(spdlog::level::trace); s_ClientLogger = std::make_shared("APP", appSinks.begin(), appSinks.end()); s_ClientLogger->set_level(spdlog::level::trace); - /* + s_EditorConsoleLogger = std::make_shared("Console", editorConsoleSinks.begin(), editorConsoleSinks.end()); - s_EditorConsoleLogger->set_level(spdlog::level::trace);*/ + s_EditorConsoleLogger->set_level(spdlog::level::trace); SetDefaultTagSettings(); } diff --git a/Core/Source/Lux/Core/Log.h b/Core/Source/Lux/Core/Log.h index ea633be..6c689e1 100644 --- a/Core/Source/Lux/Core/Log.h +++ b/Core/Source/Lux/Core/Log.h @@ -10,27 +10,17 @@ #include #include #include -#include - -// LUX_DIST may not be defined in Debug/Release builds -#ifndef LUX_DIST - #define LUX_DIST 0 -#endif #define LUX_ASSERT_MESSAGE_BOX (!LUX_DIST && LUX_PLATFORM_WINDOWS && false) #if LUX_ASSERT_MESSAGE_BOX - #ifdef LUX_PLATFORM_WINDOWS - #include - #endif +#ifdef LUX_PLATFORM_WINDOWS +#include +#endif #endif namespace Lux { - // Callback function type for console message logging - // Parameters: level, message - using LogCallback = std::function; - class Log { public: @@ -60,10 +50,6 @@ namespace Lux { static std::map& EnabledTags() { return s_EnabledTags; } static void SetDefaultTagSettings(); - // Console callback registration - static void SetConsoleCallback(LogCallback callback) { s_ConsoleCallback = std::move(callback); } - static void ClearConsoleCallback() { s_ConsoleCallback = nullptr; } - #if defined(LUX_PLATFORM_WINDOWS) template static void PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args); @@ -88,11 +74,11 @@ namespace Lux { { switch (level) { - case Level::Trace: return "Trace"; - case Level::Info: return "Info"; - case Level::Warn: return "Warn"; - case Level::Error: return "Error"; - case Level::Fatal: return "Fatal"; + case Level::Trace: return "Trace"; + case Level::Info: return "Info"; + case Level::Warn: return "Warn"; + case Level::Error: return "Error"; + case Level::Fatal: return "Fatal"; } return ""; } @@ -108,21 +94,12 @@ namespace Lux { } private: - // Internal helper to invoke the console callback - static void NotifyConsoleCallback(Level level, const std::string& message) - { - if (s_ConsoleCallback) - s_ConsoleCallback(static_cast(level), message); - } - static std::shared_ptr s_CoreLogger; static std::shared_ptr s_ClientLogger; static std::shared_ptr s_EditorConsoleLogger; inline static std::map s_EnabledTags; static std::map s_DefaultTagDetails; - - inline static LogCallback s_ConsoleCallback; }; } @@ -148,13 +125,11 @@ namespace Lux { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Core Logging -#define LUX_CORE_TRACE(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Trace, __VA_ARGS__) -#define LUX_CORE_INFO(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Info, __VA_ARGS__) -#define LUX_CORE_WARN(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Warn, __VA_ARGS__) -#define LUX_CORE_ERROR(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Error, __VA_ARGS__) -#define LUX_CORE_FATAL(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Fatal, __VA_ARGS__) -#define LUX_CORE_CRITICAL(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Fatal, __VA_ARGS__) - +#define LUX_CORE_TRACE(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Trace, __VA_ARGS__) +#define LUX_CORE_INFO(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Info, __VA_ARGS__) +#define LUX_CORE_WARN(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Warn, __VA_ARGS__) +#define LUX_CORE_ERROR(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Error, __VA_ARGS__) +#define LUX_CORE_FATAL(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Core, ::Lux::Log::Level::Fatal, __VA_ARGS__) // Client Logging #define LUX_TRACE(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Client, ::Lux::Log::Level::Trace, __VA_ARGS__) #define LUX_INFO(...) ::Lux::Log::PrintMessage(::Lux::Log::Type::Client, ::Lux::Log::Level::Info, __VA_ARGS__) @@ -183,29 +158,24 @@ namespace Lux { if (detail.Enabled && detail.LevelFilter <= level) { auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); - std::string formatted = std::format(format, std::forward(args)...); - switch (level) { case Level::Trace: - logger->trace("{}", formatted); + logger->trace(format, std::forward(args)...); break; case Level::Info: - logger->info("{}", formatted); + logger->info(format, std::forward(args)...); break; case Level::Warn: - logger->warn("{}", formatted); + logger->warn(format, std::forward(args)...); break; case Level::Error: - logger->error("{}", formatted); + logger->error(format, std::forward(args)...); break; case Level::Fatal: - logger->critical("{}", formatted); + logger->critical(format, std::forward(args)...); break; } - - // Notify the console callback - NotifyConsoleCallback(level, formatted); } } @@ -218,29 +188,24 @@ namespace Lux { { auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); std::string formatted = std::format(format, std::forward(args)...); - std::string taggedMessage = std::format("[{}] {}", tag, formatted); - switch (level) { - case Level::Trace: - logger->trace("{}", taggedMessage); - break; - case Level::Info: - logger->info("{}", taggedMessage); - break; - case Level::Warn: - logger->warn("{}", taggedMessage); - break; - case Level::Error: - logger->error("{}", taggedMessage); - break; - case Level::Fatal: - logger->critical("{}", taggedMessage); - break; + case Level::Trace: + logger->trace("[{0}] {1}", tag, formatted); + break; + case Level::Info: + logger->info("[{0}] {1}", tag, formatted); + break; + case Level::Warn: + logger->warn("[{0}] {1}", tag, formatted); + break; + case Level::Error: + logger->error("[{0}] {1}", tag, formatted); + break; + case Level::Fatal: + logger->critical("[{0}] {1}", tag, formatted); + break; } - - // Notify the console callback - NotifyConsoleCallback(level, taggedMessage); } } @@ -251,29 +216,24 @@ namespace Lux { if (detail.Enabled && detail.LevelFilter <= level) { auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); - std::string taggedMessage = std::format("[{}] {}", tag, message); - switch (level) { - case Level::Trace: - logger->trace("{}", taggedMessage); - break; - case Level::Info: - logger->info("{}", taggedMessage); - break; - case Level::Warn: - logger->warn("{}", taggedMessage); - break; - case Level::Error: - logger->error("{}", taggedMessage); - break; - case Level::Fatal: - logger->critical("{}", taggedMessage); - break; + case Level::Trace: + logger->trace("[{0}] {1}", tag, message); + break; + case Level::Info: + logger->info("[{0}] {1}", tag, message); + break; + case Level::Warn: + logger->warn("[{0}] {1}", tag, message); + break; + case Level::Error: + logger->error("[{0}] {1}", tag, message); + break; + case Level::Fatal: + logger->critical("[{0}] {1}", tag, message); + break; } - - // Notify the console callback - NotifyConsoleCallback(level, taggedMessage); } } diff --git a/Core/Source/Lux/Core/Window.cpp b/Core/Source/Lux/Core/Window.cpp index 44a5adb..2f04713 100644 --- a/Core/Source/Lux/Core/Window.cpp +++ b/Core/Source/Lux/Core/Window.cpp @@ -155,13 +155,8 @@ namespace Lux { if (!m_Specification.Decorated) { - // This removes titlebar on all platforms - // and all of the native window effects on non-Windows platforms -#ifdef LUX_PLATFORM_WINDOWS - glfwWindowHint(GLFW_TITLEBAR, false); -#else + // Disable native decorations so editor can render a fully custom titlebar. glfwWindowHint(GLFW_DECORATED, false); -#endif } m_WindowHandle = glfwCreateWindow((int)m_Specification.Width, (int)m_Specification.Height, m_Data.Title.c_str(), m_Specification.Fullscreen ? glfwGetPrimaryMonitor() : nullptr, nullptr); diff --git a/Core/Source/Lux/Editor/EditorConsole/ConsoleMessage.h b/Core/Source/Lux/Editor/EditorConsole/ConsoleMessage.h new file mode 100644 index 0000000..ee416ec --- /dev/null +++ b/Core/Source/Lux/Editor/EditorConsole/ConsoleMessage.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Lux/Core/Base.h" + +#include +#include + +namespace Lux { + + enum class ConsoleMessageFlags : int16_t + { + None = -1, + Info = BIT(0), + Warning = BIT(1), + Error = BIT(2), + + All = Info | Warning | Error + }; + + struct ConsoleMessage + { + std::string ShortMessage; + std::string LongMessage; + int16_t Flags; + + time_t Time; + }; + +} diff --git a/Core/Source/Lux/Editor/EditorConsole/EditorConsoleSink.h b/Core/Source/Lux/Editor/EditorConsole/EditorConsoleSink.h new file mode 100644 index 0000000..ad74453 --- /dev/null +++ b/Core/Source/Lux/Editor/EditorConsole/EditorConsoleSink.h @@ -0,0 +1,89 @@ +#pragma once + +#include "Lux/Editor/EditorConsolePanel.h" + +#include + +#include +#include + +namespace Lux { + + class EditorConsoleSink : public spdlog::sinks::base_sink + { + public: + explicit EditorConsoleSink(uint32_t bufferCapacity) + : m_MessageBufferCapacity(bufferCapacity), m_MessageBuffer(bufferCapacity) { + } + + virtual ~EditorConsoleSink() = default; + + EditorConsoleSink(const EditorConsoleSink& other) = delete; + EditorConsoleSink& operator=(const EditorConsoleSink& other) = delete; + + protected: + void sink_it_(const spdlog::details::log_msg& msg) override + { + spdlog::memory_buf_t formatted; + spdlog::sinks::base_sink::formatter_->format(msg, formatted); + std::string longMessage = formatted; + std::string shortMessage = longMessage; + + if (shortMessage.length() > 100) + { + size_t spacePos = shortMessage.find_first_of(' ', 100); + if (spacePos != std::string::npos) + shortMessage.replace(spacePos, shortMessage.length() - 1, "..."); + } + + m_MessageBuffer[m_MessageCount++] = ConsoleMessage{ shortMessage, longMessage, GetMessageFlags(msg.level), std::chrono::system_clock::to_time_t(msg.time) }; + + if (m_MessageCount == m_MessageBufferCapacity) + flush_(); + } + + void flush_() override + { + for (const auto& message : m_MessageBuffer) + EditorConsolePanel::PushMessage(message); + + m_MessageCount = 0; + } + + private: + static int16_t GetMessageFlags(spdlog::level::level_enum level) + { + int16_t flags = 0; + + switch (level) + { + case spdlog::level::trace: + case spdlog::level::debug: + case spdlog::level::info: + { + flags |= (int16_t)ConsoleMessageFlags::Info; + break; + } + case spdlog::level::warn: + { + flags |= (int16_t)ConsoleMessageFlags::Warning; + break; + } + case spdlog::level::err: + case spdlog::level::critical: + { + flags |= (int16_t)ConsoleMessageFlags::Error; + break; + } + } + + return flags; + } + + private: + uint32_t m_MessageBufferCapacity; + std::vector m_MessageBuffer; + uint32_t m_MessageCount = 0; + }; + +} diff --git a/Core/Source/Lux/Editor/EditorConsolePanel.cpp b/Core/Source/Lux/Editor/EditorConsolePanel.cpp new file mode 100644 index 0000000..1e72841 --- /dev/null +++ b/Core/Source/Lux/Editor/EditorConsolePanel.cpp @@ -0,0 +1,257 @@ +#include "lpch.h" +#include "EditorConsolePanel.h" + +#include "Lux/Core/Application.h" +//#include "Lux/Core/Events/SceneEvents.h" +#include "Lux/Editor/EditorResources.h" +#include "Lux/Editor/FontAwesome.h" +#include "Lux/ImGui/Colors.h" +#include "Lux/ImGui/ImGuiEx.h" + +#include + +#include + +namespace Lux { + + static EditorConsolePanel* s_Instance = nullptr; + + static const ImVec4 s_InfoTint = ImVec4(0.0f, 0.431372549f, 1.0f, 1.0f); + static const ImVec4 s_WarningTint = ImVec4(1.0f, 0.890196078f, 0.0588235294f, 1.0f); + static const ImVec4 s_ErrorTint = ImVec4(1.0f, 0.309803922f, 0.309803922f, 1.0f); + + EditorConsolePanel::EditorConsolePanel() + { + LUX_CORE_ASSERT(s_Instance == nullptr); + s_Instance = this; + + m_MessageBuffer.reserve(500); + } + + EditorConsolePanel::~EditorConsolePanel() + { + s_Instance = nullptr; + } + + void EditorConsolePanel::OnEvent(Event& event) + {/* + EventDispatcher dispatcher(event); + dispatcher.Dispatch([this](ScenePreStartEvent& e) + { + if (m_ClearOnPlay) + { + std::scoped_lock lock(m_MessageBufferMutex); + m_MessageBuffer.clear(); + } + return false; + });*/ + } + + void EditorConsolePanel::OnImGuiRender(bool& isOpen) + { + if (ImGui::Begin(m_PanelName, &isOpen)) + { + ImVec2 consoleSize = ImGui::GetContentRegionAvail(); + consoleSize.y -= 32.0f; + + RenderMenu({ consoleSize.x, 28.0f }); + RenderConsole(consoleSize); + } + ImGui::End(); + } + + void EditorConsolePanel::OnProjectChanged(const Ref& project) + { + std::scoped_lock lock(m_MessageBufferMutex); + m_MessageBuffer.clear(); + } + + void EditorConsolePanel::Focus() + { + ImGui::SetWindowFocus(m_PanelName); + } + + void EditorConsolePanel::SetProgress(const std::string& label, float progress) + { + m_ProgressLabel = label; + m_Progress = progress; + + if (m_Progress >= 1.0f) + ClearProgress(); + } + + void EditorConsolePanel::ClearProgress() + { + m_ProgressLabel = std::string(); + m_Progress = 0.0f; + } + + void EditorConsolePanel::RenderMenu(const ImVec2& size) + { + ImGuiEx::ScopedStyleStack frame(ImGuiStyleVar_FrameBorderSize, 0.0f, ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::BeginChild("Toolbar", size); + + const float ToolbarHeight = 28.0f; + + if (ImGui::Button("Clear", { 75.0f, ToolbarHeight })) + { + std::scoped_lock lock(m_MessageBufferMutex); + m_MessageBuffer.clear(); + } + + ImGui::SameLine(); + + const auto& style = ImGui::GetStyle(); + const std::string clearOnPlayText = std::format("{} Clear on Play", m_ClearOnPlay ? LUX_ICON_CHECK : LUX_ICON_TIMES); + ImVec4 textColor = m_ClearOnPlay ? style.Colors[ImGuiCol_Text] : style.Colors[ImGuiCol_TextDisabled]; + if (ImGuiEx::ColoredButton(clearOnPlayText.c_str(), GetToolbarButtonColor(m_ClearOnPlay), textColor, ImVec2(110.0f, ToolbarHeight))) + m_ClearOnPlay = !m_ClearOnPlay; + + if (!m_ProgressLabel.empty()) + { + ImGui::SameLine(); + std::string progressBarText = std::format("{} ({}%)", m_ProgressLabel, (int)(m_Progress * 100 + 0.01f)); + ImGui::ProgressBar(m_Progress, ImVec2(ImGui::GetContentRegionAvail().x / 2.0f, ToolbarHeight), progressBarText.c_str()); + } + + { + const ImVec2 buttonSize(ToolbarHeight, ToolbarHeight); + + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100.0f, 0.0f); + textColor = (m_MessageFilters & (int16_t)ConsoleMessageFlags::Info) ? s_InfoTint : style.Colors[ImGuiCol_TextDisabled]; + if (ImGuiEx::ColoredButton(LUX_ICON_INFO_CIRCLE, GetToolbarButtonColor(m_MessageFilters & (int16_t)ConsoleMessageFlags::Info), textColor, buttonSize)) + m_MessageFilters ^= (int16_t)ConsoleMessageFlags::Info; + + ImGui::SameLine(); + textColor = (m_MessageFilters & (int16_t)ConsoleMessageFlags::Warning) ? s_WarningTint : style.Colors[ImGuiCol_TextDisabled]; + if (ImGuiEx::ColoredButton(LUX_ICON_EXCLAMATION_TRIANGLE, GetToolbarButtonColor(m_MessageFilters & (int16_t)ConsoleMessageFlags::Warning), textColor, buttonSize)) + m_MessageFilters ^= (int16_t)ConsoleMessageFlags::Warning; + + ImGui::SameLine(); + textColor = (m_MessageFilters & (int16_t)ConsoleMessageFlags::Error) ? s_ErrorTint : style.Colors[ImGuiCol_TextDisabled]; + if (ImGuiEx::ColoredButton(LUX_ICON_EXCLAMATION_CIRCLE, GetToolbarButtonColor(m_MessageFilters & (int16_t)ConsoleMessageFlags::Error), textColor, buttonSize)) + m_MessageFilters ^= (int16_t)ConsoleMessageFlags::Error; + } + + ImGui::EndChild(); + } + + void EditorConsolePanel::RenderConsole(const ImVec2& size) + { + static const char* s_Columns[] = { "Type", "Timestamp", "Message" }; + + ImGuiEx::Table("Console", s_Columns, 3, size, [&]() + { + std::scoped_lock lock(m_MessageBufferMutex); + + float scrollY = ImGui::GetScrollY(); + if (scrollY < m_PreviousScrollY) + m_EnableScrollToLatest = false; + + if (scrollY >= ImGui::GetScrollMaxY()) + m_EnableScrollToLatest = true; + + m_PreviousScrollY = scrollY; + + float rowHeight = 24.0f; + for (uint32_t i = 0; i < m_MessageBuffer.size(); i++) + { + const auto& msg = m_MessageBuffer[i]; + + if (!(m_MessageFilters & (int16_t)msg.Flags)) + continue; + + ImGui::PushID(&msg); + + const bool clicked = ImGuiEx::TableRowClickable(msg.ShortMessage.c_str(), rowHeight); + + ImGuiEx::Separator(ImVec2(4.0f, ImGui::CalcTextSize(msg.ShortMessage.c_str()).y), GetMessageColor(msg)); + ImGui::SameLine(); + ImGui::Text(GetMessageType(msg)); + ImGui::TableNextColumn(); + ImGuiEx::ShiftCursorX(4.0f); + + std::stringstream timeString; + tm* timeBuffer = localtime(&msg.Time); + timeString << std::put_time(timeBuffer, "%T"); + ImGui::Text(timeString.str().c_str()); + + ImGui::TableNextColumn(); + ImGuiEx::ShiftCursorX(4.0f); + ImGui::Text(msg.ShortMessage.c_str()); + + if (i == m_MessageBuffer.size() - 1 && m_ScrollToLatest) + { + ImGui::ScrollToItem(); + m_ScrollToLatest = false; + } + + if (clicked) + { + ImGui::OpenPopup("Detailed Message"); + auto [width, height] = Application::Get().GetWindow().GetSize(); + auto [xPos, yPos] = Application::Get().GetWindow().GetWindowPos(); + //ImVec2 size = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ (float)width * 0.5f, (float)height * 0.5f }); + ImGui::SetNextWindowPos({ xPos + (float)width / 2.0f, yPos + (float)height / 2.5f }, 0, { 0.5, 0.5 }); + m_DetailedPanelOpen = true; + } + + if (m_DetailedPanelOpen) + { + ImGuiEx::ScopedStyle windowPadding(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f)); + ImGuiEx::ScopedStyle framePadding(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 8.0f)); + + if (ImGui::BeginPopupModal("Detailed Message", &m_DetailedPanelOpen, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped(msg.LongMessage.c_str()); + if (ImGui::Button("Copy To Clipboard", ImVec2(120.0f, 28.0f))) + { + ImGui::SetClipboardText(msg.LongMessage.c_str()); + } + ImGui::EndPopup(); + } + } + + ImGui::PopID(); + } + }); + } + + const char* EditorConsolePanel::GetMessageType(const ConsoleMessage& message) const + { + if (message.Flags & (int16_t)ConsoleMessageFlags::Info) return "Info"; + if (message.Flags & (int16_t)ConsoleMessageFlags::Warning) return "Warning"; + if (message.Flags & (int16_t)ConsoleMessageFlags::Error) return "Error"; + return "Unknown"; + } + + const ImVec4& EditorConsolePanel::GetMessageColor(const ConsoleMessage& message) const + { + //if (message.Flags & (int16_t)ConsoleMessageFlags::Info) return s_InfoButtonOnTint; + if (message.Flags & (int16_t)ConsoleMessageFlags::Warning) return s_WarningTint; + if (message.Flags & (int16_t)ConsoleMessageFlags::Error) return s_ErrorTint; + return s_InfoTint; + } + + ImVec4 EditorConsolePanel::GetToolbarButtonColor(const bool value) const + { + const auto& style = ImGui::GetStyle(); + return value ? style.Colors[ImGuiCol_Header] : style.Colors[ImGuiCol_FrameBg]; + } + + void EditorConsolePanel::PushMessage(const ConsoleMessage& message) + { + if (s_Instance == nullptr) + return; + + { + std::scoped_lock lock(s_Instance->m_MessageBufferMutex); + s_Instance->m_MessageBuffer.push_back(message); + } + + if (s_Instance->m_EnableScrollToLatest) + s_Instance->m_ScrollToLatest = true; + } + +} diff --git a/Core/Source/Lux/Editor/EditorConsolePanel.h b/Core/Source/Lux/Editor/EditorConsolePanel.h new file mode 100644 index 0000000..ddef782 --- /dev/null +++ b/Core/Source/Lux/Editor/EditorConsolePanel.h @@ -0,0 +1,57 @@ +#pragma once + +#include "EditorPanel.h" + +#include "EditorConsole/ConsoleMessage.h" + +#include "Lux/Renderer/Texture.h" + +#include + +namespace Lux { + class EditorConsolePanel : public EditorPanel + { + public: + EditorConsolePanel(); + ~EditorConsolePanel(); + + virtual void OnEvent(Event& e) override; + virtual void OnImGuiRender(bool& isOpen) override; + virtual void OnProjectChanged(const Ref& project) override; + + void Focus(); + + void SetProgress(const std::string& label, float progress); + void ClearProgress(); + private: + void RenderMenu(const ImVec2& size); + void RenderConsole(const ImVec2& size); + const char* GetMessageType(const ConsoleMessage& message) const; + const ImVec4& GetMessageColor(const ConsoleMessage& message) const; + ImVec4 GetToolbarButtonColor(const bool value) const; + + private: + static void PushMessage(const ConsoleMessage& message); + + private: + const char* m_PanelName = "Log"; + bool m_ClearOnPlay = true; + + std::mutex m_MessageBufferMutex; + std::vector m_MessageBuffer; + + bool m_EnableScrollToLatest = true; + bool m_ScrollToLatest = false; + float m_PreviousScrollY = 0.0f; + + int16_t m_MessageFilters = (int16_t)ConsoleMessageFlags::All; + + bool m_DetailedPanelOpen = false; + + std::string m_ProgressLabel; + float m_Progress = 0.0f; + private: + friend class EditorConsoleSink; + }; + +} diff --git a/Core/Source/Lux/ImGui/ImGuiEx.h b/Core/Source/Lux/ImGui/ImGuiEx.h index 770c79c..76f7ad0 100644 --- a/Core/Source/Lux/ImGui/ImGuiEx.h +++ b/Core/Source/Lux/ImGui/ImGuiEx.h @@ -10,7 +10,7 @@ #include "Lux/ImGui/ImGuiFonts.h" #include "Lux/ImGui/ImGuiUtilities.h" #include "Lux/ImGui/ImGuiWidgets.h" -//#include "Lux/Scene/Prefab.h" +#include "Lux/Scene/Prefab.h" #include "Lux/Scene/Scene.h" #include "Lux/Utilities/StringUtils.h" @@ -2802,7 +2802,7 @@ namespace Lux::ImGuiEx { uintptr_t tempLength = length; FieldType nativeType = arrayStorage->GetFieldInfo()->Type; - if (UI::PropertyInput("Length", tempLength, 1, 1, ImGuiInputTextFlags_EnterReturnsTrue)) + if (PropertyInput("Length", tempLength, 1, 1, ImGuiInputTextFlags_EnterReturnsTrue)) { arrayStorage->Resize((uint32_t)tempLength); length = tempLength; @@ -2827,12 +2827,12 @@ namespace Lux::ImGuiEx { size_t dataSize = ImGui::DataTypeGetInfo(dataType)->Size; if (components > 1) { - if (UI::DragScalarN(GenerateID(), dataType, &data, components, 1.0f, (const void*)0, (const void*)0, format, 0)) + if (DragScalarN(GenerateID(), dataType, &data, components, 1.0f, (const void*)0, (const void*)0, format, 0)) arrayStorage->SetValue>((uint32_t)index, data); } else { - if (UI::DragScalar(GenerateID(), dataType, &data, 1.0f, (const void*)0, (const void*)0, format, (ImGuiSliderFlags)0)) + if (DragScalar(GenerateID(), dataType, &data, 1.0f, (const void*)0, (const void*)0, format, (ImGuiSliderFlags)0)) arrayStorage->SetValue>((uint32_t)index, data); } @@ -2863,7 +2863,7 @@ namespace Lux::ImGuiEx { const float buttonSize = ImGui::GetFrameHeight(); ImGui::SetNextItemWidth(ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x))); - if (UI::InputText(GenerateID(), &data)) + if (InputText(GenerateID(), &data)) arrayStorage->SetValue((uint32_t)index, data); const ImVec2 backupFramePadding = style.FramePadding; @@ -2892,7 +2892,7 @@ namespace Lux::ImGuiEx { case FieldType::Bool: { bool value = arrayStorage->GetValue(i); - if (UI::Property(indexString.c_str(), value)) + if (Property(indexString.c_str(), value)) { arrayStorage->SetValue(i, value); modified = true; diff --git a/Core/Source/Lux/ImGui/ImGuiLayer.cpp b/Core/Source/Lux/ImGui/ImGuiLayer.cpp index 56f3d4a..dc79433 100644 --- a/Core/Source/Lux/ImGui/ImGuiLayer.cpp +++ b/Core/Source/Lux/ImGui/ImGuiLayer.cpp @@ -189,7 +189,7 @@ namespace Lux { void ImGuiLayer::Begin() { - ImGui::SetMouseCursor(Input::GetCursorMode() == CursorMode::Normal ? ImGui::GetMouseCursor() : ImGuiMouseCursor_None); + ImGui::SetMouseCursor(Input::GetCursorMode() == CursorMode::Normal ? ImGuiMouseCursor_Arrow : ImGuiMouseCursor_None); m_ImGuiRenderer->UpdateFontTexture(); ImGui_ImplGlfw_NewFrame(); @@ -202,8 +202,7 @@ namespace Lux { { ImGui::Render(); - m_ImGuiRenderer->RenderToSwapchain(ImGui::GetMainViewport(), - &Application::Get().GetWindow().GetSwapChain()); + m_ImGuiRenderer->RenderToSwapchain(ImGui::GetMainViewport(), &Application::Get().GetWindow().GetSwapChain()); // Update and Render additional Platform Windows if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) @@ -292,7 +291,6 @@ namespace Lux { colors[ImGuiCol_Tab] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); colors[ImGuiCol_TabHovered] = ImColor(255, 225, 135, 30); colors[ImGuiCol_TabActive] = ImColor(255, 225, 135, 60); - colors[ImGuiCol_TabSelectedOverline] = ImColor(255, 225, 135, 60); colors[ImGuiCol_TabUnfocused] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); colors[ImGuiCol_TabUnfocusedActive] = colors[ImGuiCol_TabHovered]; @@ -312,12 +310,12 @@ namespace Lux { colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.0f); colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.0f); - // Check Mark - Orange accent - colors[ImGuiCol_CheckMark] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent); + // Check Mark + colors[ImGuiCol_CheckMark] = ImColor(200, 200, 200, 255); - // Slider - Orange accent - colors[ImGuiCol_SliderGrab] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent); - colors[ImGuiCol_SliderGrabActive] = ImVec4(1.0f, 0.7f, 0.3f, 1.0f); + // Slider + colors[ImGuiCol_SliderGrab] = ImVec4(0.51f, 0.51f, 0.51f, 0.7f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.66f, 0.66f, 0.66f, 1.0f); // Text colors[ImGuiCol_Text] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::text); @@ -343,19 +341,11 @@ namespace Lux { // Menubar colors[ImGuiCol_MenuBarBg] = ImVec4{ 0.0f, 0.0f, 0.0f, 0.0f }; - // Docking - colors[ImGuiCol_DockingPreview] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent); - - // Nav highlight - colors[ImGuiCol_NavHighlight] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent); - //======================================================== /// Style - style.FrameRounding = 4.0f; + style.FrameRounding = 2.5f; style.FrameBorderSize = 1.0f; style.IndentSpacing = 11.0f; - style.ItemSpacing = ImVec2(8.0f, 4.0f); - style.WindowPadding = ImVec2(8.0f, 8.0f); } void ImGuiLayer::AllowInputEvents(bool allowEvents) diff --git a/Core/Source/Lux/Renderer/Renderer2D.cpp b/Core/Source/Lux/Renderer/Renderer2D.cpp index d5f4864..e9df7fb 100644 --- a/Core/Source/Lux/Renderer/Renderer2D.cpp +++ b/Core/Source/Lux/Renderer/Renderer2D.cpp @@ -254,8 +254,17 @@ namespace Lux { { ShaderDataType::Float2, "a_LocalPosition" }, { ShaderDataType::Float4, "a_Color" } }; - m_CirclePipeline = Pipeline::Create(pipelineSpecification); - m_CircleMaterial = Material::Create(pipelineSpecification.Shader); + + RenderPassSpecification circleSpec; + circleSpec.DebugName = "Renderer2D-Circle"; + circleSpec.Pipeline = Pipeline::Create(pipelineSpecification); + m_CirclePass = RenderPass::Create(circleSpec); + m_CirclePass->SetInput("Camera", m_UBSCamera); + LUX_CORE_VERIFY(m_CirclePass->Validate()); + m_CirclePass->Bake(); + + m_CirclePipeline = m_CirclePass->GetPipeline(); + m_CircleMaterial = Material::Create(pipelineSpecification.Shader, "CircleMaterial"); m_CircleVertexBuffers.resize(1); m_CircleVertexBufferBases.resize(1); @@ -265,7 +274,7 @@ namespace Lux { m_CircleVertexBufferBases[0].resize(framesInFlight); for (uint32_t i = 0; i < framesInFlight; i++) { - uint64_t allocationSize = c_MaxVertices * sizeof(QuadVertex); + uint64_t allocationSize = c_MaxVertices * sizeof(CircleVertex); m_CircleVertexBuffers[0][i] = VertexBuffer::Create(allocationSize); m_MemoryStats.TotalAllocated += allocationSize; m_CircleVertexBufferBases[0][i] = lnew CircleVertex[c_MaxVertices]; @@ -428,8 +437,26 @@ namespace Lux { } } + // Filled circles + for (uint32_t i = 0; i <= m_CircleBufferWriteIndex; i++) + { + dataSize = (uint32_t)((uint8_t*)m_CircleVertexBufferPtr[i] - (uint8_t*)m_CircleVertexBufferBases[i][frameIndex]); + if (dataSize) + { + uint32_t indexCount = i == m_CircleBufferWriteIndex ? m_CircleIndexCount - (c_MaxIndices * i) : c_MaxIndices; + m_CircleVertexBuffers[i][frameIndex]->SetData(m_CircleVertexBufferBases[i][frameIndex], dataSize); + + Renderer::BeginRenderPass(m_RenderCommandBuffer, m_CirclePass); + Renderer::RenderGeometry(m_RenderCommandBuffer, m_CirclePass->GetPipeline(), m_CircleMaterial, m_CircleVertexBuffers[i][frameIndex], m_QuadIndexBuffer, glm::mat4(1.0f), indexCount); + Renderer::EndRenderPass(m_RenderCommandBuffer); + + m_DrawStats.DrawCalls++; + m_MemoryStats.Used += dataSize; + } + } + // Lines (depth-tested) - m_LinePass->GetPipeline()->GetSpecification().DepthTest = true; + m_LinePass->GetPipeline()->GetSpecification().DepthTest = m_DepthTest; for (uint32_t i = 0; i <= m_LineBufferWriteIndex; i++) { dataSize = (uint32_t)((uint8_t*)m_LineVertexBufferPtr[i] - (uint8_t*)m_LineVertexBufferBases[i][frameIndex]); @@ -477,12 +504,15 @@ namespace Lux { Ref Renderer2D::GetTargetRenderPass() { - return nullptr; + return m_QuadPass; } void Renderer2D::SetTargetFramebuffer(Ref framebuffer) { - if (framebuffer != m_TextPass->GetTargetFramebuffer()) + if (!framebuffer) + return; + + if (framebuffer != m_QuadPass->GetTargetFramebuffer()) { { PipelineSpecification pipelineSpec = m_QuadPass->GetSpecification().Pipeline->GetSpecification(); @@ -498,6 +528,7 @@ namespace Lux { { PipelineSpecification pipelineSpec = m_LinePass->GetSpecification().Pipeline->GetSpecification(); pipelineSpec.TargetFramebuffer = framebuffer; + pipelineSpec.LineWidth = m_LineWidth; RenderPassSpecification& renderpassSpec = m_LinePass->GetSpecification(); renderpassSpec.Pipeline = Pipeline::Create(pipelineSpec); // FIX: re-validate and re-bake @@ -516,6 +547,17 @@ namespace Lux { LUX_CORE_VERIFY(m_TextPass->Validate()); m_TextPass->Bake(); } + + { + PipelineSpecification pipelineSpec = m_CirclePass->GetSpecification().Pipeline->GetSpecification(); + pipelineSpec.TargetFramebuffer = framebuffer; + RenderPassSpecification& renderpassSpec = m_CirclePass->GetSpecification(); + renderpassSpec.Pipeline = Pipeline::Create(pipelineSpec); + m_CirclePass->SetInput("Camera", m_UBSCamera); + LUX_CORE_VERIFY(m_CirclePass->Validate()); + m_CirclePass->Bake(); + m_CirclePipeline = m_CirclePass->GetPipeline(); + } } } @@ -1115,10 +1157,10 @@ namespace Lux { bufferPtr->LocalPosition = m_QuadVertexPositions[i] * 2.0f; bufferPtr->Color = color; bufferPtr++; - - m_CircleIndexCount += 6; - m_DrawStats.QuadCount++; } + + m_CircleIndexCount += 6; + m_DrawStats.QuadCount++; } void Renderer2D::DrawLine(const glm::vec3& p0, const glm::vec3& p1, const glm::vec4& color, const bool onTop) diff --git a/Core/Source/Lux/Renderer/Renderer2D.h b/Core/Source/Lux/Renderer/Renderer2D.h index ba3a00d..1c8615e 100644 --- a/Core/Source/Lux/Renderer/Renderer2D.h +++ b/Core/Source/Lux/Renderer/Renderer2D.h @@ -170,6 +170,7 @@ namespace Lux { std::vector m_QuadVertexBufferPtr; uint32_t m_QuadBufferWriteIndex = 0; + Ref m_CirclePass; Ref m_CirclePipeline; Ref m_CircleMaterial; std::vector m_CircleVertexBuffers; diff --git a/Core/Source/Lux/Renderer/SceneRenderer.cpp b/Core/Source/Lux/Renderer/SceneRenderer.cpp index 2fd4f16..60170d2 100644 --- a/Core/Source/Lux/Renderer/SceneRenderer.cpp +++ b/Core/Source/Lux/Renderer/SceneRenderer.cpp @@ -72,6 +72,8 @@ namespace Lux { m_UBSShadow = UniformBufferSet::Create(sizeof(UBShadow)); m_UBSRendererData = UniformBufferSet::Create(sizeof(UBRendererData)); m_UBSPointLights = UniformBufferSet::Create(sizeof(UBPointLights)); + m_UBSSpotLights = UniformBufferSet::Create(sizeof(UBSpotLights)); + m_UBSSpotShadow = UniformBufferSet::Create(sizeof(UBSpotShadow)); // ── Storage buffer sets (start with generous initial capacity) ───────── { @@ -87,6 +89,15 @@ namespace Lux { spec.DebugName = "ObjectIndexes"; m_SBSObjectIndexes = StorageBufferSet::Create(spec, sizeof(uint32_t) * 4096); } + { + StorageBufferSpecification indexSpec; + indexSpec.GPUOnly = false; + indexSpec.DebugName = "VisiblePointLightIndices"; + m_SBSVisiblePointLightIndices = StorageBufferSet::Create(indexSpec, sizeof(uint32_t) * 1024); + + indexSpec.DebugName = "VisibleSpotLightIndices"; + m_SBSVisibleSpotLightIndices = StorageBufferSet::Create(indexSpec, sizeof(uint32_t) * 1024); + } // Common vertex layout for all opaque mesh pipelines VertexBufferLayout vertexLayout = { @@ -218,6 +229,10 @@ namespace Lux { m_GeometryPass->SetInput("ShadowData", m_UBSShadow); m_GeometryPass->SetInput("RendererData", m_UBSRendererData); m_GeometryPass->SetInput("PointLightData", m_UBSPointLights); + m_GeometryPass->SetInput("SpotLightData", m_UBSSpotLights); + m_GeometryPass->SetInput("SpotShadowData", m_UBSSpotShadow); + m_GeometryPass->SetInput("VisiblePointLightIndicesBuffer", m_SBSVisiblePointLightIndices); + m_GeometryPass->SetInput("VisibleSpotLightIndicesBuffer", m_SBSVisibleSpotLightIndices); m_GeometryPass->SetInput("InstanceTransforms", m_SBSInstanceTransforms); m_GeometryPass->SetInput("ObjectIndexes", m_SBSObjectIndexes); // Environment textures – overridden each frame in BeginScene once env is set @@ -226,6 +241,7 @@ namespace Lux { m_GeometryPass->SetInput("u_BRDFLUTTexture", Renderer::GetBRDFLutTexture()); // Shadow map output from the shadow pass above m_GeometryPass->SetInput("u_ShadowMapTexture", m_ShadowMapPass->GetDepthOutput()); + m_GeometryPass->SetInput("u_SpotShadowTexture", Renderer::GetWhiteTexture()); // or a dummy 2D array if required LUX_CORE_VERIFY(m_GeometryPass->Validate()); m_GeometryPass->Bake(); @@ -239,12 +255,19 @@ namespace Lux { m_GeometryPassTransparent->SetInput("ShadowData", m_UBSShadow); m_GeometryPassTransparent->SetInput("RendererData", m_UBSRendererData); m_GeometryPassTransparent->SetInput("PointLightData", m_UBSPointLights); + m_GeometryPassTransparent->SetInput("SpotLightData", m_UBSSpotLights); + m_GeometryPassTransparent->SetInput("SpotShadowData", m_UBSSpotShadow); + m_GeometryPassTransparent->SetInput("VisiblePointLightIndicesBuffer", m_SBSVisiblePointLightIndices); + m_GeometryPassTransparent->SetInput("VisibleSpotLightIndicesBuffer", m_SBSVisibleSpotLightIndices); m_GeometryPassTransparent->SetInput("InstanceTransforms", m_SBSInstanceTransforms); m_GeometryPassTransparent->SetInput("ObjectIndexes", m_SBSObjectIndexes); + // Environment textures – overridden each frame in BeginScene once env is set m_GeometryPassTransparent->SetInput("u_EnvRadianceTex", Renderer::GetBlackCubeTexture()); m_GeometryPassTransparent->SetInput("u_EnvIrradianceTex", Renderer::GetBlackCubeTexture()); m_GeometryPassTransparent->SetInput("u_BRDFLUTTexture", Renderer::GetBRDFLutTexture()); + // Shadow map output from the shadow pass above m_GeometryPassTransparent->SetInput("u_ShadowMapTexture", m_ShadowMapPass->GetDepthOutput()); + m_GeometryPassTransparent->SetInput("u_SpotShadowTexture", Renderer::GetWhiteTexture()); // or a dummy 2D array if required LUX_CORE_VERIFY(m_GeometryPassTransparent->Validate()); m_GeometryPassTransparent->Bake(); } diff --git a/Core/Source/Lux/Renderer/SceneRenderer.h b/Core/Source/Lux/Renderer/SceneRenderer.h index 37a1762..0ff71dc 100644 --- a/Core/Source/Lux/Renderer/SceneRenderer.h +++ b/Core/Source/Lux/Renderer/SceneRenderer.h @@ -367,6 +367,8 @@ namespace Lux { Ref m_SBSInstanceTransforms; // TransformVertexData[] Ref m_SBSObjectIndexes; // uint32_t[] – maps draw → transform + Ref m_SBSVisiblePointLightIndices; + Ref m_SBSVisibleSpotLightIndices; // ── Shadow map (single ortho cascade) ──────────────────────────────── Ref m_ShadowMapPass; diff --git a/Core/Source/Lux/Scene/Components.h b/Core/Source/Lux/Scene/Components.h index 683a64a..9ab0d07 100644 --- a/Core/Source/Lux/Scene/Components.h +++ b/Core/Source/Lux/Scene/Components.h @@ -81,6 +81,15 @@ namespace Lux { } }; + struct RelationshipComponent + { + UUID ParentHandle = 0; + std::vector Children; + + RelationshipComponent() = default; + RelationshipComponent(const RelationshipComponent&) = default; + }; + struct SpriteRendererComponent { glm::vec4 Color{ 1.0f, 1.0f, 1.0f, 1.0f }; @@ -282,6 +291,37 @@ namespace Lux { // 3D RENDERING COMPONENTS // ============================================================================ + struct MeshComponent + { + AssetHandle Mesh = 0; + AssetHandle MaterialTable = 0; + bool Visible = true; + bool CastShadows = true; + + MeshComponent() = default; + MeshComponent(const MeshComponent&) = default; + }; + + struct MeshTagComponent + { + std::string MeshName; + + MeshTagComponent() = default; + MeshTagComponent(const MeshTagComponent&) = default; + MeshTagComponent(const std::string& meshName) + : MeshName(meshName) { + } + }; + + struct PrefabComponent + { + AssetHandle PrefabID = 0; + UUID EntityID = 0; + + PrefabComponent() = default; + PrefabComponent(const PrefabComponent&) = default; + }; + struct StaticMeshComponent { AssetHandle Mesh = 0; @@ -349,10 +389,10 @@ namespace Lux { }; using AllComponents = - ComponentGroup; + MeshComponent, MeshTagComponent, PrefabComponent, StaticMeshComponent, DirectionalLightComponent, PointLightComponent, SpotLightComponent, SkyLightComponent>; } diff --git a/Core/Source/Lux/Scene/Entity.cpp b/Core/Source/Lux/Scene/Entity.cpp index 98b02eb..c942ada 100644 --- a/Core/Source/Lux/Scene/Entity.cpp +++ b/Core/Source/Lux/Scene/Entity.cpp @@ -2,12 +2,70 @@ #include "Lux/Scene/Entity.h" #include "Lux/Scene/ScriptableEntity.h" +#include + namespace Lux { - Entity::Entity(entt::entity handle, Scene* scene) - : m_EntityHandle(handle), m_Scene(scene) + Entity Entity::GetParent() const + { + if (!m_Scene || !m_Scene->m_Registry.template has(m_EntityHandle)) + return {}; + + const auto& relationship = m_Scene->m_Registry.get(m_EntityHandle); + if (relationship.ParentHandle == 0) + return {}; + + return m_Scene->GetEntityByUUID(relationship.ParentHandle); + } + + void Entity::SetParent(Entity parent) + { + if (!*this || !m_Scene) + return; + + if (parent && parent.m_Scene != m_Scene) + return; + + if (parent == *this) + return; + + auto& relationship = GetComponent(); + const UUID selfUUID = GetUUID(); + + // Prevent cycles + for (Entity current = parent; current; current = current.GetParent()) + { + if (current == *this) + return; + } + + Entity currentParent = GetParent(); + if (currentParent) + { + auto& siblings = currentParent.GetComponent().Children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), selfUUID), siblings.end()); + } + + relationship.ParentHandle = 0; + + if (!parent) + return; + + auto& parentRelationship = parent.GetComponent(); + if (std::find(parentRelationship.Children.begin(), parentRelationship.Children.end(), selfUUID) == parentRelationship.Children.end()) + parentRelationship.Children.emplace_back(selfUUID); + + relationship.ParentHandle = parent.GetUUID(); + } + + std::vector& Entity::Children() { + return GetComponent().Children; + } + bool Entity::HasParent() const + { + return (bool)GetParent(); } ScriptComponent::ScriptComponent() = default; diff --git a/Core/Source/Lux/Scene/Entity.h b/Core/Source/Lux/Scene/Entity.h index 1c4df4b..e5f3ad6 100644 --- a/Core/Source/Lux/Scene/Entity.h +++ b/Core/Source/Lux/Scene/Entity.h @@ -12,8 +12,9 @@ namespace Lux { public: Entity() = default; - Entity(entt::entity handle, Scene* scene); - Entity(const Entity& other) = default; + Entity(entt::entity handle, Scene* scene) + : m_EntityHandle(handle), m_Scene(scene) { + } template T& AddComponent(Args&&... args) @@ -58,6 +59,11 @@ namespace Lux UUID GetUUID() { return GetComponent().ID; } const std::string& GetName() { return GetComponent().Tag; } + Scene* GetScene() const { return m_Scene; } + Entity GetParent() const; + void SetParent(Entity parent); + std::vector& Children(); + bool HasParent() const; bool operator==(const Entity& other) const { diff --git a/Core/Source/Lux/Scene/Prefab.cpp b/Core/Source/Lux/Scene/Prefab.cpp new file mode 100644 index 0000000..d97a6d2 --- /dev/null +++ b/Core/Source/Lux/Scene/Prefab.cpp @@ -0,0 +1,71 @@ +#include "lpch.h" +#include "Lux/Scene/Prefab.h" + +#include "Lux/Scene/Components.h" +#include "Lux/Scene/Entity.h" +#include "Lux/Scene/Scene.h" + +namespace Lux { + + template + static void CopyComponentIfExists(Entity dst, Entity src) + { + ([&]() + { + if (src.HasComponent()) + dst.AddOrReplaceComponent(src.GetComponent()); + }(), ...); + } + + template + static void CopyComponentIfExists(ComponentGroup, Entity dst, Entity src) + { + CopyComponentIfExists(dst, src); + } + + using PrefabCloneComponents = + ComponentGroup; + + Prefab::Prefab() + { + m_Scene = Ref::Create(); + } + + Entity Prefab::CreatePrefabFromEntity(Entity entity) + { + LUX_CORE_ASSERT(entity, "Cannot create prefab from null entity!"); + + m_Scene = Ref::Create(); + m_RootEntityID = 0; + + std::function duplicateHierarchy; + duplicateHierarchy = [&](Entity source, Entity parent) -> Entity + { + Entity destination = m_Scene->CreateEntity(source.GetName()); + CopyComponentIfExists(PrefabCloneComponents{}, destination, source); + + if (parent) + destination.SetParent(parent); + + if (source.HasComponent()) + { + for (const UUID childID : source.Children()) + { + Entity child = source.GetScene()->GetEntityByUUID(childID); + if (child) + duplicateHierarchy(child, destination); + } + } + + return destination; + }; + + Entity root = duplicateHierarchy(entity, {}); + m_RootEntityID = root.GetUUID(); + return root; + } + +} diff --git a/Core/Source/Lux/Scene/Prefab.h b/Core/Source/Lux/Scene/Prefab.h new file mode 100644 index 0000000..d34b567 --- /dev/null +++ b/Core/Source/Lux/Scene/Prefab.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Lux/Asset/Asset.h" +#include "Lux/Core/UUID.h" + +namespace Lux { + + class Scene; + class Entity; + + class Prefab : public Asset + { + public: + Prefab(); + + static AssetType GetStaticType() { return AssetType::Prefab; } + virtual AssetType GetAssetType() const override { return GetStaticType(); } + + Entity CreatePrefabFromEntity(Entity entity); + + const Ref& GetScene() const { return m_Scene; } + UUID GetRootEntityID() const { return m_RootEntityID; } + + private: + Ref m_Scene; + UUID m_RootEntityID = 0; + }; + +} diff --git a/Core/Source/Lux/Scene/Scene.cpp b/Core/Source/Lux/Scene/Scene.cpp index 28dc162..5f1fe02 100644 --- a/Core/Source/Lux/Scene/Scene.cpp +++ b/Core/Source/Lux/Scene/Scene.cpp @@ -9,6 +9,7 @@ #include "Lux/Scene/Components.h" #include "Lux/Scene/Entity.h" +#include "Lux/Scene/Prefab.h" #include "Lux/Scene/ScriptableEntity.h" #include "Lux/Scripting/ScriptEngine.h" #include "Lux/Renderer/Renderer2D.h" @@ -45,16 +46,16 @@ namespace Lux { static void CopyComponent(entt::registry& dst, entt::registry& src, const std::unordered_map& enttMap) { ([&]() - { - auto view = src.view(); - for (auto srcEntity : view) { - entt::entity dstEntity = enttMap.at(src.get(srcEntity).ID); + auto view = src.view(); + for (auto srcEntity : view) + { + entt::entity dstEntity = enttMap.at(src.get(srcEntity).ID); - auto& srcComponent = src.get(srcEntity); - dst.emplace_or_replace(dstEntity, srcComponent); - } - }(), ...); + auto& srcComponent = src.get(srcEntity); + dst.emplace_or_replace(dstEntity, srcComponent); + } + }(), ...); } template @@ -67,10 +68,10 @@ namespace Lux { static void CopyComponentIfExists(Entity dst, Entity src) { ([&]() - { - if (src.HasComponent()) - dst.AddOrReplaceComponent(src.GetComponent()); - }(), ...); + { + if (src.HasComponent()) + dst.AddOrReplaceComponent(src.GetComponent()); + }(), ...); } template @@ -116,6 +117,7 @@ namespace Lux { Entity entity = { m_Registry.create(), this }; entity.AddComponent(uuid); entity.AddComponent(); + entity.AddComponent(); auto& tag = entity.AddComponent(); tag.Tag = name.empty() ? "Entity" : name; @@ -139,15 +141,18 @@ namespace Lux { { auto filter = m_Registry.view(); - filter.each([&](TransformComponent& transform, AudioListenerComponent& ac) + filter.each([&](entt::entity entityHandle, TransformComponent&, AudioListenerComponent& ac) { ac.Listener = Ref::Create(); if (ac.Active) { - const glm::mat4 inverted = glm::inverse(transform.GetTransform()); + Entity entity = { entityHandle, this }; + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(entity); + const glm::mat4 inverted = glm::inverse(worldTransform); + const glm::vec3 worldPosition = glm::vec3(worldTransform[3]); const glm::vec3 forward = glm::normalize(glm::vec3(inverted[2].x, inverted[2].y, inverted[2].z)); ac.Listener->SetConfig(ac.Config); - ac.Listener->SetPosition(glm::vec4(transform.Translation, 1.0f)); + ac.Listener->SetPosition(glm::vec4(worldPosition, 1.0f)); ac.Listener->SetDirection(glm::vec3{ -forward.x, -forward.y, -forward.z }); } }); @@ -155,20 +160,24 @@ namespace Lux { { auto view = m_Registry.view(); - view.each([&](TransformComponent& transform, AudioSourceComponent& ac) + view.each([&](entt::entity entityHandle, TransformComponent&, AudioSourceComponent& ac) { if (AssetManager::IsAssetHandleValid(ac.Audio)) { + Entity entity = { entityHandle, this }; + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(entity); + const glm::vec3 worldPosition = glm::vec3(worldTransform[3]); + const glm::mat4 inverted = glm::inverse(worldTransform); + const glm::vec3 forward = glm::normalize(glm::vec3(inverted[2].x, inverted[2].y, inverted[2].z)); + if (ac.Audio && !ac.AudioSourceData.UsePlaylist) { Ref audioSource = AssetManager::GetAsset(ac.Audio); - const glm::mat4 inverted = glm::inverse(transform.GetTransform()); - const glm::vec3 forward = glm::normalize(glm::vec3(inverted[2].x, inverted[2].y, inverted[2].z)); if (audioSource != nullptr) { audioSource->SetConfig(ac.Config); - audioSource->SetPosition(glm::vec4(transform.Translation, 1.0f)); + audioSource->SetPosition(glm::vec4(worldPosition, 1.0f)); audioSource->SetDirection(forward); if (ac.Config.PlayOnAwake) audioSource->Play(); @@ -182,13 +191,11 @@ namespace Lux { if (ac.AudioSourceData.CurrentIndex < ac.AudioSourceData.Playlist.size()) { Ref playingSourceIndex = AssetManager::GetAsset(ac.AudioSourceData.Playlist[ac.AudioSourceData.CurrentIndex]); - const glm::mat4 inverted = glm ::inverse(transform.GetTransform()); - const glm::vec3 forward = glm::normalize(glm::vec3(inverted[2].x, inverted[2].y, inverted[2].z)); if (playingSourceIndex != nullptr) { playingSourceIndex->SetConfig(ac.Config); - playingSourceIndex->SetPosition(glm::vec4(transform.Translation, 1.0f)); + playingSourceIndex->SetPosition(glm::vec4(worldPosition, 1.0f)); playingSourceIndex->SetDirection(forward); if (ac.Config.PlayOnAwake) playingSourceIndex->Play(); @@ -348,13 +355,14 @@ namespace Lux { { Entity e = { entity, this }; auto& ac = e.GetComponent(); - auto& transform = e.GetComponent(); if (ac.Active) { - const glm::mat4 inverted = glm::inverse(transform.GetTransform()); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(e); + const glm::mat4 inverted = glm::inverse(worldTransform); + const glm::vec3 worldPosition = glm::vec3(worldTransform[3]); const glm::vec3 forward = glm::normalize(glm::vec3(inverted[2].x, inverted[2].y, inverted[2].z)); - ac.Listener->SetPosition(glm::vec4(transform.Translation, 1.0f)); + ac.Listener->SetPosition(glm::vec4(worldPosition, 1.0f)); ac.Listener->SetDirection(glm::vec3{ -forward.x, -forward.y, -forward.z }); //break; } @@ -365,13 +373,11 @@ namespace Lux { LUX_PROFILE_SCOPE_COLOR("Scene::OnUpdateRuntime::AudioSourceComponent Scope", 0xFF7200); auto view = m_Registry.view(); - view.each([&](entt::entity entity, TransformComponent& transform, AudioSourceComponent& asc) + view.each([&](entt::entity entityHandle, TransformComponent&, AudioSourceComponent& asc) { - //Entity e = { entity, this }; - //auto& transform = e.GetComponent(); - - //const glm::mat4 inverted = glm::inverse(transform.GetTransform()); - //const glm::vec3 forward = glm::vector_normalize3(inverted.Value.z_axis); + Entity entity = { entityHandle, this }; + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(entity); + const glm::vec3 worldPosition = glm::vec3(worldTransform[3]); if (asc.Audio && !asc.AudioSourceData.UsePlaylist) { @@ -387,7 +393,7 @@ namespace Lux { } audioSource->SetConfig(asc.Config); - audioSource->SetPosition(glm::vec4(transform.Translation, 1.0f)); + audioSource->SetPosition(glm::vec4(worldPosition, 1.0f)); } else if (asc.Audio && asc.AudioSourceData.UsePlaylist) { @@ -419,7 +425,7 @@ namespace Lux { { currentSource->SetConfig(asc.Config); currentSource->Play(); - currentSource->SetPosition(glm::vec4(transform.Translation, 1.0f)); + currentSource->SetPosition(glm::vec4(worldPosition, 1.0f)); asc.AudioSourceData.PlayingCurrentIndex = true; asc.Paused = false; @@ -447,13 +453,14 @@ namespace Lux { { Entity e = { acEntity, this }; auto& ac = e.GetComponent(); - auto& transform = e.GetComponent(); if (ac.Active) { - const glm::mat4 inverted = glm::inverse(transform.GetTransform()); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(e); + const glm::mat4 inverted = glm::inverse(worldTransform); + const glm::vec3 worldPosition = glm::vec3(worldTransform[3]); const glm::vec3 forward = glm::normalize(glm::vec3(inverted[2].x, inverted[2].y, inverted[2].z)); - ac.Listener->SetPosition(glm::vec4(transform.Translation, 1.0f)); + ac.Listener->SetPosition(glm::vec4(worldPosition, 1.0f)); ac.Listener->SetDirection(glm::vec3{ -forward.x, -forward.y, -forward.z }); } }); @@ -466,8 +473,7 @@ namespace Lux { view.each([&](entt::entity entity, AudioSourceComponent& asc) { - Entity e = { entity , this}; - auto& transform = e.GetComponent(); + Entity e = { entity , this }; if (asc.Audio) { @@ -523,12 +529,12 @@ namespace Lux { auto view = m_Registry.view(); for (auto entity : view) { - auto [transform, camera] = view.get(entity); - + auto& camera = view.get(entity); if (camera.Primary) + { mainCamera = &camera.Camera; - cameraTransform = transform.GetTransform(); + cameraTransform = GetWorldSpaceTransformMatrix(Entity{ entity, this }); break; } } @@ -537,20 +543,21 @@ namespace Lux { if (mainCamera) { glm::mat4 view = glm::inverse(cameraTransform); - m_Renderer2D->BeginScene(mainCamera->GetProjectionMatrix()* view, view); + m_Renderer2D->BeginScene(mainCamera->GetProjectionMatrix() * view, view); // Draw sprites { auto group = m_Registry.group(entt::get); for (auto entity : group) { - auto [transform, sprite] = group.get(entity); + auto& sprite = group.get(entity); Ref texture = AssetManager::GetAsset(sprite.Texture); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(Entity{ entity, this }); if (texture) - m_Renderer2D->DrawQuad(transform.GetTransform(), texture, sprite.TilingFactor, sprite.Color); + m_Renderer2D->DrawQuad(worldTransform, texture, sprite.TilingFactor, sprite.Color); else - m_Renderer2D->DrawQuad(transform.GetTransform(), sprite.Color); // fallback to solid color + m_Renderer2D->DrawQuad(worldTransform, sprite.Color); // fallback to solid color } } @@ -559,9 +566,9 @@ namespace Lux { auto view = m_Registry.view(); for (auto entity : view) { - auto [transform, circle] = view.get(entity); + auto& circle = view.get(entity); - m_Renderer2D->DrawCircle(transform.GetTransform(), circle.Color); + m_Renderer2D->DrawCircle(GetWorldSpaceTransformMatrix(Entity{ entity, this }), circle.Color); } } @@ -570,12 +577,12 @@ namespace Lux { auto view = m_Registry.view(); for (auto entity : view) { - auto [transform, text] = view.get(entity); + auto& text = view.get(entity); Ref font = AssetManager::GetAsset(text.FontHandle); if (font) { - m_Renderer2D->DrawString(text.TextString, font, transform.GetTransform(), text.MaxWidth, text.Color, text.LineSpacing, text.Kerning); + m_Renderer2D->DrawString(text.TextString, font, GetWorldSpaceTransformMatrix(Entity{ entity, this }), text.MaxWidth, text.Color, text.LineSpacing, text.Kerning); } } } @@ -672,6 +679,54 @@ namespace Lux { return newEntity; } + Entity Scene::InstantiatePrefab(Ref prefab) + { + if (!prefab) + return {}; + + const Ref& prefabScene = prefab->GetScene(); + if (!prefabScene) + return {}; + + Entity prefabRoot = prefabScene->GetEntityByUUID(prefab->GetRootEntityID()); + if (!prefabRoot) + return {}; + + using PrefabInstantiationComponents = + ComponentGroup; + + std::function instantiateHierarchy; + instantiateHierarchy = [&](Entity source, Entity parent) -> Entity + { + Entity destination = CreateEntity(source.GetName()); + CopyComponentIfExists(PrefabInstantiationComponents{}, destination, source); + + auto& prefabComponent = destination.AddOrReplaceComponent(); + prefabComponent.PrefabID = prefab->Handle; + prefabComponent.EntityID = source.GetUUID(); + + if (parent) + destination.SetParent(parent); + + if (source.HasComponent()) + { + for (const UUID childID : source.Children()) + { + Entity child = prefabScene->GetEntityByUUID(childID); + if (child) + instantiateHierarchy(child, destination); + } + } + + return destination; + }; + + return instantiateHierarchy(prefabRoot, {}); + } + Entity Scene::FindEntityByName(std::string_view name) { auto view = m_Registry.view(); @@ -684,14 +739,35 @@ namespace Lux { return {}; } - Entity Scene::GetEntityByUUID(UUID uuid) + Entity Scene::GetEntityByUUID(UUID uuid) const { if (m_EntityMap.find(uuid) != m_EntityMap.end()) - return { m_EntityMap.at(uuid), this }; + return { m_EntityMap.at(uuid), const_cast(this) }; return {}; } + glm::mat4 Scene::GetWorldSpaceTransformMatrix(Entity entity) const + { + if (!entity || !entity.HasComponent()) + return glm::mat4(1.0f); + + glm::mat4 transform = entity.GetComponent().GetTransform(); + + if (entity.HasComponent()) + { + const auto& relationship = entity.GetComponent(); + if (relationship.ParentHandle != 0) + { + Entity parent = GetEntityByUUID(relationship.ParentHandle); + if (parent) + transform = GetWorldSpaceTransformMatrix(parent) * transform; + } + } + + return transform; + } + void Scene::OnPhysics2DStart() { // Guard against double-call leak @@ -769,12 +845,13 @@ namespace Lux { auto group = m_Registry.group(entt::get); for (auto entity : group) { - auto [transform, sprite] = group.get(entity); + auto& sprite = group.get(entity); Ref texture = AssetManager::GetAsset(sprite.Texture); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(Entity{ entity, this }); if (texture) - m_Renderer2D->DrawQuad(transform.GetTransform(), texture, sprite.TilingFactor, sprite.Color); + m_Renderer2D->DrawQuad(worldTransform, texture, sprite.TilingFactor, sprite.Color); else - m_Renderer2D->DrawQuad(transform.GetTransform(), sprite.Color); + m_Renderer2D->DrawQuad(worldTransform, sprite.Color); } } @@ -783,8 +860,8 @@ namespace Lux { auto view = m_Registry.view(); for (auto entity : view) { - auto [transform, circle] = view.get(entity); - m_Renderer2D->DrawCircle(transform.GetTransform(), circle.Color); + auto& circle = view.get(entity); + m_Renderer2D->DrawCircle(GetWorldSpaceTransformMatrix(Entity{ entity, this }), circle.Color); } } // Draw text @@ -792,11 +869,11 @@ namespace Lux { auto view = m_Registry.view(); for (auto entity : view) { - auto [transform, text] = view.get(entity); + auto& text = view.get(entity); Ref font = AssetManager::GetAsset(text.FontHandle); if (font) { - m_Renderer2D->DrawString(text.TextString, font, transform.GetTransform(), text.MaxWidth, text.Color, text.LineSpacing, text.Kerning); + m_Renderer2D->DrawString(text.TextString, font, GetWorldSpaceTransformMatrix(Entity{ entity, this }), text.MaxWidth, text.Color, text.LineSpacing, text.Kerning); } } } @@ -821,16 +898,10 @@ namespace Lux { if (dirLightIndex >= LightEnvironment::MaxDirectionalLights) break; - const auto& transform = view.get(entity); const auto& dirLight = view.get(entity); - - // Calculate direction from rotation (forward vector) - glm::vec3 rotation = transform.Rotation; - glm::vec3 direction = glm::normalize(glm::vec3( - cos(rotation.y) * cos(rotation.x), - sin(rotation.x), - sin(rotation.y) * cos(rotation.x) - )); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(Entity{ entity, const_cast(this) }); + + glm::vec3 direction = glm::normalize(glm::vec3(worldTransform * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f))); lightEnv.DirectionalLights[dirLightIndex].Direction = direction; lightEnv.DirectionalLights[dirLightIndex].Radiance = dirLight.Radiance; @@ -847,11 +918,11 @@ namespace Lux { auto view = m_Registry.view(); for (auto entity : view) { - const auto& transform = view.get(entity); const auto& pointLight = view.get(entity); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(Entity{ entity, const_cast(this) }); PointLight pl; - pl.Position = transform.Translation; + pl.Position = glm::vec3(worldTransform[3]); pl.Radiance = pointLight.Radiance; pl.Intensity = pointLight.Intensity; pl.Radius = pointLight.Radius; @@ -875,7 +946,7 @@ namespace Lux { for (auto entity : view) { const auto& skyLight = view.get(entity); - + if (skyLight.EnvironmentMap) { outIntensity = skyLight.Intensity; @@ -886,14 +957,13 @@ namespace Lux { return nullptr; } - void Scene::SubmitStaticMeshes(Ref renderer, - const std::function& isSelected) const + void Scene::SubmitStaticMeshes(Ref renderer, + const std::function& isSelected) const { auto view = m_Registry.view(); for (auto e : view) { Entity entity = { e, const_cast(this) }; - const auto& transform = view.get(e); const auto& meshComp = view.get(e); if (!meshComp.Visible) @@ -932,7 +1002,7 @@ namespace Lux { staticMesh, meshSource, materialTable, - transform.GetTransform(), + GetWorldSpaceTransformMatrix(entity), nullptr, // no override material selected ); @@ -940,7 +1010,7 @@ namespace Lux { } void Scene::Render3D(const EditorCamera& camera, Ref renderer, - const std::function& isSelected) + const std::function& isSelected) { if (!renderer || !renderer->IsReady()) return; @@ -971,6 +1041,63 @@ namespace Lux { // End the frame and execute the render passes renderer->EndScene(); + + // Composite 2D content onto the SceneRenderer output so the main viewport + // can display a single image containing both 3D and 2D, like Hazel. + if (renderer->GetFinalPassImage()) + { + Ref renderer2D = renderer->GetRenderer2D(); + if (renderer2D) + { + renderer2D->ResetStats(); + renderer2D->BeginScene(camera.GetViewProjection(), camera.GetViewMatrix()); + renderer2D->SetTargetFramebuffer(renderer->GetExternalCompositeFramebuffer()); + + // Draw sprites + { + auto group = m_Registry.group(entt::get); + for (auto entity : group) + { + auto& sprite = group.get(entity); + + Ref texture = AssetManager::GetAsset(sprite.Texture); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(Entity{ entity, this }); + if (texture) + renderer2D->DrawQuad(worldTransform, texture, sprite.TilingFactor, sprite.Color); + else + renderer2D->DrawQuad(worldTransform, sprite.Color); + } + } + + // Draw circles + { + auto view = m_Registry.view(); + for (auto entity : view) + { + auto& circle = view.get(entity); + renderer2D->DrawCircle(GetWorldSpaceTransformMatrix(Entity{ entity, this }), circle.Color); + } + } + + // Draw text + { + auto view = m_Registry.view(); + for (auto entity : view) + { + auto& text = view.get(entity); + + Ref font = AssetManager::GetAsset(text.FontHandle); + if (font) + { + renderer2D->DrawString(text.TextString, font, GetWorldSpaceTransformMatrix(Entity{ entity, this }), + text.MaxWidth, text.Color, text.LineSpacing, text.Kerning); + } + } + } + + renderer2D->EndScene(); + } + } } void Scene::Render3DRuntime(Ref renderer) @@ -984,13 +1111,11 @@ namespace Lux { return; const auto& cameraComp = cameraEntity.GetComponent(); - const auto& transformComp = cameraEntity.GetComponent(); // Set up the SceneRendererCamera from the runtime camera SceneRendererCamera sceneCamera; sceneCamera.Camera.SetProjectionMatrix(cameraComp.Camera.GetProjectionMatrix(), cameraComp.Camera.GetUnReversedProjectionMatrix()); - sceneCamera.ViewMatrix = glm::inverse(transformComp.GetTransform()); - // Note: Near/Far/FOV from runtime camera component if needed + sceneCamera.ViewMatrix = glm::inverse(GetWorldSpaceTransformMatrix(cameraEntity)); // Begin the 3D rendering frame renderer->BeginScene(sceneCamera); @@ -1010,6 +1135,65 @@ namespace Lux { // End the frame renderer->EndScene(); + + // Composite 2D content onto the SceneRenderer output. + if (renderer->GetFinalPassImage()) + { + Ref renderer2D = renderer->GetRenderer2D(); + if (renderer2D) + { + const glm::mat4 view = sceneCamera.ViewMatrix; + const glm::mat4 viewProjection = cameraComp.Camera.GetProjectionMatrix() * view; + + renderer2D->ResetStats(); + renderer2D->BeginScene(viewProjection, view); + renderer2D->SetTargetFramebuffer(renderer->GetExternalCompositeFramebuffer()); + + // Draw sprites + { + auto group = m_Registry.group(entt::get); + for (auto entity : group) + { + auto& sprite = group.get(entity); + + Ref texture = AssetManager::GetAsset(sprite.Texture); + const glm::mat4 worldTransform = GetWorldSpaceTransformMatrix(Entity{ entity, this }); + if (texture) + renderer2D->DrawQuad(worldTransform, texture, sprite.TilingFactor, sprite.Color); + else + renderer2D->DrawQuad(worldTransform, sprite.Color); + } + } + + // Draw circles + { + auto view = m_Registry.view(); + for (auto entity : view) + { + auto& circle = view.get(entity); + renderer2D->DrawCircle(GetWorldSpaceTransformMatrix(Entity{ entity, this }), circle.Color); + } + } + + // Draw text + { + auto view = m_Registry.view(); + for (auto entity : view) + { + auto& text = view.get(entity); + + Ref font = AssetManager::GetAsset(text.FontHandle); + if (font) + { + renderer2D->DrawString(text.TextString, font, GetWorldSpaceTransformMatrix(Entity{ entity, this }), + text.MaxWidth, text.Color, text.LineSpacing, text.Kerning); + } + } + } + + renderer2D->EndScene(); + } + } } // ============================================================================ @@ -1031,6 +1215,11 @@ namespace Lux { { } + template<> + void Scene::OnComponentAdded(Entity entity, RelationshipComponent& component) + { + } + template<> void Scene::OnComponentAdded(Entity entity, CameraComponent& component) { @@ -1126,6 +1315,21 @@ namespace Lux { // 3D Component specializations + template<> + void Scene::OnComponentAdded(Entity entity, MeshComponent& component) + { + } + + template<> + void Scene::OnComponentAdded(Entity entity, MeshTagComponent& component) + { + } + + template<> + void Scene::OnComponentAdded(Entity entity, PrefabComponent& component) + { + } + template<> void Scene::OnComponentAdded(Entity entity, StaticMeshComponent& component) { diff --git a/Core/Source/Lux/Scene/Scene.h b/Core/Source/Lux/Scene/Scene.h index db5701c..6b26f31 100644 --- a/Core/Source/Lux/Scene/Scene.h +++ b/Core/Source/Lux/Scene/Scene.h @@ -9,6 +9,7 @@ #include "entt/entt.hpp" +#include #include class b2World; @@ -17,6 +18,7 @@ namespace Lux { class Entity; class Framebuffer; + class Prefab; class SceneRenderer; // Forward declare light structures (defined in SceneRenderer.h) @@ -49,12 +51,14 @@ namespace Lux { void OnUpdateEditor(Timestep ts, EditorCamera& camera); void OnViewportResize(uint32_t width, uint32_t height); - void SetTargetFramebuffer(Ref framebuffer); // add this + void SetTargetFramebuffer(Ref framebuffer); Entity DuplicateEntity(Entity entity); + Entity InstantiatePrefab(Ref prefab); Entity FindEntityByName(std::string_view name); - Entity GetEntityByUUID(UUID uuid); + Entity GetEntityByUUID(UUID uuid) const; + glm::mat4 GetWorldSpaceTransformMatrix(Entity entity) const; Entity GetPrimaryCameraEntity(); @@ -76,8 +80,8 @@ namespace Lux { // Submit all StaticMeshComponent entities to the SceneRenderer // Optional predicate to determine if an entity is selected (for highlight rendering) - void SubmitStaticMeshes(Ref renderer, - const std::function& isSelected = nullptr) const; + void SubmitStaticMeshes(Ref renderer, + const std::function& isSelected = nullptr) const; // High-level 3D rendering method that orchestrates the full 3D pipeline: // - Sets up camera @@ -86,18 +90,24 @@ namespace Lux { // - Submits all static meshes // Call this from EditorLayer to render 3D content void Render3D(const EditorCamera& camera, Ref renderer, - const std::function& isSelected = nullptr); + const std::function& isSelected = nullptr); // Render 3D content using a runtime camera (for Play mode) void Render3DRuntime(Ref renderer); - // ============================================================================ + // ============================================================================ template auto GetAllEntitiesWith() { return m_Registry.view(); } + + template + auto GetAllEntitiesWith() const + { + return m_Registry.view(); + } private: template void OnComponentAdded(Entity entity, T& component); diff --git a/Core/Source/Lux/Scene/SceneSerializer.cpp b/Core/Source/Lux/Scene/SceneSerializer.cpp index 0b7e801..4a6caef 100644 --- a/Core/Source/Lux/Scene/SceneSerializer.cpp +++ b/Core/Source/Lux/Scene/SceneSerializer.cpp @@ -213,6 +213,17 @@ namespace Lux { out << YAML::EndMap; // TransformComponent } + if (entity.HasComponent()) + { + out << YAML::Key << "RelationshipComponent"; + out << YAML::BeginMap; // RelationshipComponent + + const auto& relationship = entity.GetComponent(); + out << YAML::Key << "Parent" << YAML::Value << relationship.ParentHandle; + + out << YAML::EndMap; // RelationshipComponent + } + if (entity.HasComponent()) { out << YAML::Key << "CameraComponent"; @@ -380,6 +391,43 @@ namespace Lux { out << YAML::EndMap; // TextComponent } + if (entity.HasComponent()) + { + out << YAML::Key << "MeshComponent"; + out << YAML::BeginMap; + + const auto& component = entity.GetComponent(); + out << YAML::Key << "Mesh" << YAML::Value << component.Mesh; + out << YAML::Key << "MaterialTable" << YAML::Value << component.MaterialTable; + out << YAML::Key << "Visible" << YAML::Value << component.Visible; + out << YAML::Key << "CastShadows" << YAML::Value << component.CastShadows; + + out << YAML::EndMap; + } + + if (entity.HasComponent()) + { + out << YAML::Key << "MeshTagComponent"; + out << YAML::BeginMap; + + const auto& component = entity.GetComponent(); + out << YAML::Key << "MeshName" << YAML::Value << component.MeshName; + + out << YAML::EndMap; + } + + if (entity.HasComponent()) + { + out << YAML::Key << "PrefabComponent"; + out << YAML::BeginMap; + + const auto& component = entity.GetComponent(); + out << YAML::Key << "PrefabID" << YAML::Value << component.PrefabID; + out << YAML::Key << "EntityID" << YAML::Value << component.EntityID; + + out << YAML::EndMap; + } + if (entity.HasComponent()) { out << YAML::Key << "AudioSourceComponent"; @@ -573,6 +621,8 @@ namespace Lux { auto entities = data["Entities"]; if (entities) { + std::vector> pendingParentLinks; + for (auto entity : entities) { uint64_t uuid = entity["Entity"].as(); @@ -596,6 +646,14 @@ namespace Lux { tc.Scale = transformComponent["Scale"].as(); } + auto relationshipComponent = entity["RelationshipComponent"]; + if (relationshipComponent && relationshipComponent["Parent"]) + { + const UUID parentID = relationshipComponent["Parent"].as(); + if (parentID != 0) + pendingParentLinks.emplace_back(uuid, parentID); + } + auto cameraComponent = entity["CameraComponent"]; if (cameraComponent) { @@ -745,6 +803,44 @@ namespace Lux { tc.LineSpacing = textComponent["LineSpacing"].as(); } + auto meshComponent = entity["MeshComponent"]; + if (meshComponent) + { + auto& component = deserializedEntity.AddComponent(); + + if (meshComponent["Mesh"]) + component.Mesh = meshComponent["Mesh"].as(); + + if (meshComponent["MaterialTable"]) + component.MaterialTable = meshComponent["MaterialTable"].as(); + + if (meshComponent["Visible"]) + component.Visible = meshComponent["Visible"].as(); + + if (meshComponent["CastShadows"]) + component.CastShadows = meshComponent["CastShadows"].as(); + } + + auto meshTagComponent = entity["MeshTagComponent"]; + if (meshTagComponent) + { + auto& component = deserializedEntity.AddComponent(); + if (meshTagComponent["MeshName"]) + component.MeshName = meshTagComponent["MeshName"].as(); + } + + auto prefabComponent = entity["PrefabComponent"]; + if (prefabComponent) + { + auto& component = deserializedEntity.AddComponent(); + + if (prefabComponent["PrefabID"]) + component.PrefabID = prefabComponent["PrefabID"].as(); + + if (prefabComponent["EntityID"]) + component.EntityID = prefabComponent["EntityID"].as(); + } + auto audioSourceComponent = entity["AudioSourceComponent"]; if (audioSourceComponent) { @@ -957,6 +1053,14 @@ namespace Lux { component.Intensity = skyLightComponent["Intensity"].as(); } } + + for (const auto& [childID, parentID] : pendingParentLinks) + { + Entity childEntity = m_Scene->GetEntityByUUID(childID); + Entity parentEntity = m_Scene->GetEntityByUUID(parentID); + if (childEntity && parentEntity) + childEntity.SetParent(parentEntity); + } } return true; diff --git a/Core/Source/Lux/UI/UI.h b/Core/Source/Lux/UI/UI.h deleted file mode 100644 index 86dacc8..0000000 --- a/Core/Source/Lux/UI/UI.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include - -namespace Lux::UI { - - struct ScopedStyleColor - { - ScopedStyleColor() = default; - - ScopedStyleColor(ImGuiCol idx, ImVec4 color, bool predicate = true) - : m_Set(predicate) - { - if (predicate) - ImGui::PushStyleColor(idx, color); - } - - ScopedStyleColor(ImGuiCol idx, ImU32 color, bool predicate = true) - : m_Set(predicate) - { - if (predicate) - ImGui::PushStyleColor(idx, color); - } - - ~ScopedStyleColor() - { - if (m_Set) - ImGui::PopStyleColor(); - } - private: - bool m_Set = false; - }; - - -} diff --git a/Editor/Resources/Shaders/LuxPBR_Static.glsl b/Editor/Resources/Shaders/LuxPBR_Static.glsl new file mode 100644 index 0000000..ef985d7 --- /dev/null +++ b/Editor/Resources/Shaders/LuxPBR_Static.glsl @@ -0,0 +1,228 @@ +#version 450 core +#pragma stage : vert + +#include +#include +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) out VertexOutput Output; + +// Must exactly match the C++ MeshDrawPushConstants + Material data +layout(push_constant) uniform PushConstants +{ + uint ObjectIndexBase; + uint LightIndex; + uint BoneTransformBase; + uint BoneTransformStride; + + vec3 AlbedoColor; + float Metalness; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +invariant gl_Position; + +void main() +{ + mat4 transform = GetInstanceTransform(u_MaterialUniforms.ObjectIndexBase + gl_InstanceIndex); + vec4 worldPosition = transform * vec4(a_Position, 1.0); + + Output.WorldPosition = worldPosition.xyz; + Output.Normal = mat3(transform) * a_Normal; + Output.TexCoord = vec2(a_TexCoord.x, 1.0 - a_TexCoord.y); + Output.WorldNormals = mat3(transform) * mat3(a_Tangent, a_Binormal, a_Normal); + Output.WorldTransform = mat3(transform); + Output.Binormal = a_Binormal; + + Output.CameraView = mat3(u_Camera.ViewMatrix); + + vec4 shadowCoords[4]; + shadowCoords[0] = u_DirShadow.DirLightMatrices[0] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[1] = u_DirShadow.DirLightMatrices[1] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[2] = u_DirShadow.DirLightMatrices[2] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[3] = u_DirShadow.DirLightMatrices[3] * vec4(Output.WorldPosition.xyz, 1.0); + Output.ShadowMapCoords[0] = vec3(shadowCoords[0].xyz / shadowCoords[0].w); + Output.ShadowMapCoords[1] = vec3(shadowCoords[1].xyz / shadowCoords[1].w); + Output.ShadowMapCoords[2] = vec3(shadowCoords[2].xyz / shadowCoords[2].w); + Output.ShadowMapCoords[3] = vec3(shadowCoords[3].xyz / shadowCoords[3].w); + + Output.ViewPosition = vec3(u_Camera.ViewMatrix * vec4(Output.WorldPosition, 1.0)); + + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + + +#version 450 core +#pragma stage : frag + +#include +#include +#include +#include +#include +#include + +const vec3 Fdielectric = vec3(0.04); + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) in VertexOutput Input; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 o_ViewNormalsLuminance; +layout(location = 2) out vec4 o_MetalnessRoughness; + +layout(push_constant) uniform PushConstants +{ + uint ObjectIndexBase; + uint LightIndex; + uint BoneTransformBase; + uint BoneTransformStride; + + vec3 AlbedoColor; + float Metalness; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +vec3 IBL(vec3 F0, vec3 Lr) +{ + vec3 irradiance = SampleLinear(u_EnvIrradianceTex, m_Params.Normal).rgb; + vec3 F = FresnelSchlickRoughness(F0, m_Params.NdotV, m_Params.Roughness); + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseIBL = m_Params.Albedo * irradiance; + + int envRadianceTexLevels = textureQueryLevels(samplerCube(u_EnvRadianceTex, r_LinearSampler)); + vec3 specularIrradiance = SampleLinearLOD(u_EnvRadianceTex, RotateVectorAboutY(u_MaterialUniforms.EnvMapRotation, Lr), m_Params.Roughness * envRadianceTexLevels).rgb; + + vec2 specularBRDF = SampleLinear(u_BRDFLUTTexture, vec2(m_Params.NdotV, m_Params.Roughness)).rg; + vec3 specularIBL = specularIrradiance * (F0 * specularBRDF.x + specularBRDF.y); + + return kd * diffuseIBL + specularIBL; +} + +vec3 GetGradient(float value) +{ + vec3 zero = vec3(0.0, 0.0, 0.0); + vec3 white = vec3(0.0, 0.1, 0.9); + vec3 red = vec3(0.2, 0.9, 0.4); + vec3 blue = vec3(0.8, 0.8, 0.3); + vec3 green = vec3(0.9, 0.2, 0.3); + + float step0 = 0.0f; + float step1 = 2.0f; + float step2 = 4.0f; + float step3 = 8.0f; + float step4 = 16.0f; + + vec3 color = mix(zero, white, smoothstep(step0, step1, value)); + color = mix(color, white, smoothstep(step1, step2, value)); + color = mix(color, red, smoothstep(step1, step2, value)); + color = mix(color, blue, smoothstep(step2, step3, value)); + color = mix(color, green, smoothstep(step3, step4, value)); + + return color; +} + +void main() +{ + vec4 albedoTexColor = SampleLinear(u_AlbedoTexture, Input.TexCoord); + m_Params.Albedo = albedoTexColor.rgb * ToLinear(vec4(u_MaterialUniforms.AlbedoColor, 1.0)).rgb; + float alpha = albedoTexColor.a; + + m_Params.Metalness = SampleLinear(u_MetalnessTexture, Input.TexCoord).b * u_MaterialUniforms.Metalness; + m_Params.Roughness = SampleLinear(u_RoughnessTexture, Input.TexCoord).g * u_MaterialUniforms.Roughness; + o_MetalnessRoughness = vec4(m_Params.Metalness, m_Params.Roughness, 0.f, 1.f); + m_Params.Roughness = max(m_Params.Roughness, 0.05); + + m_Params.Normal = normalize(Input.Normal); + if (u_MaterialUniforms.UseNormalMap) + { + m_Params.Normal = normalize(SampleLinear(u_NormalTexture, Input.TexCoord).rgb * 2.0f - 1.0f); + m_Params.Normal = normalize(Input.WorldNormals * m_Params.Normal); + } + + o_ViewNormalsLuminance.xyz = Input.CameraView * m_Params.Normal; + + m_Params.View = normalize(u_Scene.CameraPosition - Input.WorldPosition); + m_Params.NdotV = max(dot(m_Params.Normal, m_Params.View), 0.0); + + vec3 Lr = 2.0 * m_Params.NdotV * m_Params.Normal - m_Params.View; + vec3 F0 = mix(Fdielectric, m_Params.Albedo, m_Params.Metalness); + + uint cascadeIndex = 0; + const uint SHADOW_MAP_CASCADE_COUNT = 4; + for (uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; i++) + { + if (Input.ViewPosition.z < u_RendererData.CascadeSplits[i]) + cascadeIndex = i + 1; + } + + float shadowScale = u_RendererData.SoftShadows ? + PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex), u_RendererData.LightSize) : + HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex)); + + shadowScale = 1.0 - clamp(u_Scene.DirectionalLights.ShadowAmount - shadowScale, 0.0f, 1.0f); + + vec3 lightContribution = CalculateDirLights(F0) * shadowScale; + lightContribution += CalculatePointLights(F0, Input.WorldPosition); + lightContribution += m_Params.Albedo * u_MaterialUniforms.Emission; + + vec3 iblContribution = IBL(F0, Lr) * u_Scene.EnvironmentMapIntensity; + + color = vec4(iblContribution + lightContribution, 1.0); + + if (u_Scene.DirectionalLights.Multiplier <= 0.0f) + shadowScale = 0.0f; + + o_ViewNormalsLuminance.a = clamp(shadowScale + dot(color.rgb, vec3(0.2125f, 0.7154f, 0.0721f)), 0.0f, 1.0f); + + if (u_RendererData.ShowLightComplexity) + { + int pointLightCount = GetPointLightCount(); + color.rgb = (color.rgb * 0.2) + GetGradient(float(pointLightCount)); + } +} diff --git a/Editor/Resources/Shaders/LuxPBR_Transparent.glsl b/Editor/Resources/Shaders/LuxPBR_Transparent.glsl new file mode 100644 index 0000000..a870cc3 --- /dev/null +++ b/Editor/Resources/Shaders/LuxPBR_Transparent.glsl @@ -0,0 +1,187 @@ +#version 450 core +#pragma stage : vert + +#include +#include +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) out VertexOutput Output; + +layout(push_constant) uniform PushConstants +{ + uint ObjectIndexBase; + uint LightIndex; + uint BoneTransformBase; + uint BoneTransformStride; + + vec3 AlbedoColor; + float Transparency; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +invariant gl_Position; + +void main() +{ + mat4 transform = GetInstanceTransform(u_MaterialUniforms.ObjectIndexBase + gl_InstanceIndex); + vec4 worldPosition = transform * vec4(a_Position, 1.0); + + Output.WorldPosition = worldPosition.xyz; + Output.Normal = mat3(transform) * a_Normal; + Output.TexCoord = vec2(a_TexCoord.x, 1.0 - a_TexCoord.y); + Output.WorldNormals = mat3(transform) * mat3(a_Tangent, a_Binormal, a_Normal); + Output.WorldTransform = mat3(transform); + Output.Binormal = a_Binormal; + + Output.CameraView = mat3(u_Camera.ViewMatrix); + + vec4 shadowCoords[4]; + shadowCoords[0] = u_DirShadow.DirLightMatrices[0] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[1] = u_DirShadow.DirLightMatrices[1] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[2] = u_DirShadow.DirLightMatrices[2] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[3] = u_DirShadow.DirLightMatrices[3] * vec4(Output.WorldPosition.xyz, 1.0); + Output.ShadowMapCoords[0] = vec3(shadowCoords[0].xyz / shadowCoords[0].w); + Output.ShadowMapCoords[1] = vec3(shadowCoords[1].xyz / shadowCoords[1].w); + Output.ShadowMapCoords[2] = vec3(shadowCoords[2].xyz / shadowCoords[2].w); + Output.ShadowMapCoords[3] = vec3(shadowCoords[3].xyz / shadowCoords[3].w); + + Output.ViewPosition = vec3(u_Camera.ViewMatrix * vec4(Output.WorldPosition, 1.0)); + + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + +#version 450 core +#pragma stage : frag + +#include +#include +#include +#include +#include +#include + +const vec3 Fdielectric = vec3(0.04); + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) in VertexOutput Input; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 o_ViewNormalsLuminance; +layout(location = 2) out vec4 o_MetalnessRoughness; + +layout(push_constant) uniform PushConstants +{ + uint ObjectIndexBase; + uint LightIndex; + uint BoneTransformBase; + uint BoneTransformStride; + + vec3 AlbedoColor; + float Transparency; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +vec3 IBL(vec3 F0, vec3 Lr) +{ + vec3 irradiance = SampleLinear(u_EnvIrradianceTex, m_Params.Normal).rgb; + vec3 F = FresnelSchlickRoughness(F0, m_Params.NdotV, m_Params.Roughness); + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseIBL = m_Params.Albedo * irradiance; + + int envRadianceTexLevels = textureQueryLevels(samplerCube(u_EnvRadianceTex, r_LinearSampler)); + vec3 specularIrradiance = SampleLinearLOD(u_EnvRadianceTex, RotateVectorAboutY(u_MaterialUniforms.EnvMapRotation, Lr), m_Params.Roughness * envRadianceTexLevels).rgb; + + vec2 specularBRDF = SampleLinear(u_BRDFLUTTexture, vec2(m_Params.NdotV, m_Params.Roughness)).rg; + vec3 specularIBL = specularIrradiance * (F0 * specularBRDF.x + specularBRDF.y); + + return kd * diffuseIBL + specularIBL; +} + +void main() +{ + vec4 albedoTexColor = SampleLinear(u_AlbedoTexture, Input.TexCoord); + m_Params.Albedo = albedoTexColor.rgb * ToLinear(vec4(u_MaterialUniforms.AlbedoColor, 1.0)).rgb; + + m_Params.Metalness = 0.0f; + m_Params.Roughness = max(u_MaterialUniforms.Roughness, 0.05); + + m_Params.Normal = normalize(Input.Normal); + if (u_MaterialUniforms.UseNormalMap) + { + m_Params.Normal = normalize(SampleLinear(u_NormalTexture, Input.TexCoord).rgb * 2.0f - 1.0f); + m_Params.Normal = normalize(Input.WorldNormals * m_Params.Normal); + } + + m_Params.View = normalize(u_Scene.CameraPosition - Input.WorldPosition); + m_Params.NdotV = max(dot(m_Params.Normal, m_Params.View), 0.0); + + vec3 Lr = 2.0 * m_Params.NdotV * m_Params.Normal - m_Params.View; + vec3 F0 = mix(Fdielectric, m_Params.Albedo, m_Params.Metalness); + + uint cascadeIndex = 0; + const uint SHADOW_MAP_CASCADE_COUNT = 4; + for (uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; i++) + { + if (Input.ViewPosition.z < u_RendererData.CascadeSplits[i]) + cascadeIndex = i + 1; + } + + float shadowScale = u_RendererData.SoftShadows ? + PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex), u_RendererData.LightSize) : + HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex)); + + shadowScale = 1.0 - clamp(u_Scene.DirectionalLights.ShadowAmount - shadowScale, 0.0f, 1.0f); + + vec3 lightContribution = CalculateDirLights(F0) * shadowScale; + lightContribution += CalculatePointLights(F0, Input.WorldPosition); + lightContribution += m_Params.Albedo * u_MaterialUniforms.Emission; + + vec3 iblContribution = IBL(F0, Lr) * u_Scene.EnvironmentMapIntensity; + + color = vec4(m_Params.Albedo, u_MaterialUniforms.Transparency); +} diff --git a/Editor/SandboxProject/Assets/Scenes/Physics2D.luxscene b/Editor/SandboxProject/Assets/Scenes/Physics2D.luxscene index 6ccfdd5..0c84b4e 100644 --- a/Editor/SandboxProject/Assets/Scenes/Physics2D.luxscene +++ b/Editor/SandboxProject/Assets/Scenes/Physics2D.luxscene @@ -1,5 +1,14 @@ Scene: Untitled Entities: + - Entity: 10625048149344785802 + TagComponent: + Tag: Empty Entity + TransformComponent: + Translation: [0, 0, 0] + Rotation: [0, 0, 0] + Scale: [1, 1, 1] + RelationshipComponent: + Parent: 0 - Entity: 14040563719264005740 TagComponent: Tag: Player @@ -7,6 +16,8 @@ Entities: Translation: [0, 5, 0] Rotation: [0, 0, 0] Scale: [1, 1, 1] + RelationshipComponent: + Parent: 0 SpriteRendererComponent: Color: [1, 1, 1, 1] TextureHandle: 0 @@ -27,14 +38,16 @@ Entities: TransformComponent: Translation: [0, 0, 0] Rotation: [0, 0, 0] - Scale: [1, 1, 1] + Scale: [20, 12, 10] + RelationshipComponent: + Parent: 0 CameraComponent: Camera: ProjectionType: 1 PerspectiveFOV: 0.785398185 PerspectiveNear: 0.00999999978 PerspectiveFar: 1000 - OrthographicSize: 1.20256782 + OrthographicSize: 1.62107623 OrthographicNear: -1 OrthographicFar: 1 Primary: true @@ -51,6 +64,8 @@ Entities: Translation: [0, 10, 0] Rotation: [0, 0, 0] Scale: [1, 1, 1] + RelationshipComponent: + Parent: 0 SpriteRendererComponent: Color: [1, 1, 1, 1] TextureHandle: 0 @@ -72,6 +87,8 @@ Entities: Translation: [0, -10, 0] Rotation: [0, 0, 0] Scale: [10, 1, 1] + RelationshipComponent: + Parent: 0 SpriteRendererComponent: Color: [1, 1, 1, 1] TextureHandle: 0 @@ -93,6 +110,8 @@ Entities: Translation: [0, 0, 0] Rotation: [0, 0, 0] Scale: [1, 1, 1] + RelationshipComponent: + Parent: 0 CircleRendererComponent: Color: [0.934362948, 0.497845858, 0, 1] Thickness: 1 @@ -132,6 +151,8 @@ Entities: Translation: [-3.79999995, 1.79999995, 0] Rotation: [0, 0, 0] Scale: [1, 1, 1] + RelationshipComponent: + Parent: 0 TextComponent: TextString: Welcome to Lux Color: [1, 1, 1, 1] diff --git a/Editor/Source/EditorLayer.cpp b/Editor/Source/EditorLayer.cpp index 3687426..721f34d 100644 --- a/Editor/Source/EditorLayer.cpp +++ b/Editor/Source/EditorLayer.cpp @@ -11,12 +11,15 @@ #include "Lux/Asset/AssetManager.h" #include "Lux/Asset/TextureImporter.h" #include "Lux/Asset/SceneImporter.h" +#include "Lux/Core/Math/Ray.h" +#include "Lux/Renderer/Mesh.h" #include #include #include #include "imgui/imgui_internal.h" +#include #include "ImGuizmo.h" #include "Lux/Debug/Profiler.h" #include "Lux/Editor/EditorResources.h" @@ -30,6 +33,9 @@ #include "Panels/SceneHierarchyPanel.h" #include "Panels/ContentBrowserPanel.h" #include "Panels/SceneRendererPanel.h" +#include +#include +#include namespace Lux { @@ -66,6 +72,24 @@ namespace Lux { auto* imguiRenderer = Lux::Application::Get().GetImGuiLayer()->GetImGuiRenderer(); return imguiRenderer->CreateFrameTexture(image->GetHandle().Get(), nvrhi::AllSubresources); } + + std::string GetSceneDisplayName(const std::filesystem::path& scenePath) + { + if (scenePath.empty()) + return "Untitled Scene"; + + return scenePath.stem().string(); + } + + std::string GetProjectDisplayName() + { + Ref activeProject = Project::GetActive(); + if (!activeProject) + return "No Project"; + + const auto& name = activeProject->GetConfig().Name; + return name.empty() ? "Untitled Project" : name; + } } struct SceneRendererRuntimeState @@ -117,7 +141,7 @@ namespace Lux { static bool s_ShowFontAtlasInStats = false; EditorLayer::EditorLayer() - : Layer("EditorLayer"), m_EditorCamera(60.0f, 1600.0f, 900.0f, 0.1f, 10000.0f) ,m_SquareColor({ 0.2f, 0.3f, 0.8f, 1.0f }) + : Layer("EditorLayer"), m_EditorCamera(60.0f, 1600.0f, 900.0f, 0.1f, 10000.0f), m_SquareColor({ 0.2f, 0.3f, 0.8f, 1.0f }) { s_Font = Font::GetDefaultFont(); } @@ -126,17 +150,17 @@ namespace Lux { { LUX_PROFILE_FUNCTION("EditorLayer::OnAttach"); + EditorResources::Init(); + /////////// Configure Panels /////////// m_PanelManager = CreateScope(); m_SceneHierarchyPanel = m_PanelManager->AddPanel(PanelCategory::View, SCENE_HIERARCHY_PANEL_ID, "Scene Hierarchy", true); m_PanelManager->AddPanel(PanelCategory::View, CONTENT_BROWSER_PANEL_ID, "Content Browser", true); m_PanelManager->AddPanel(PanelCategory::View, "TextEditorPanel", "Text Editor", true); + m_ConsolePanel = m_PanelManager->AddPanel(PanelCategory::View, CONSOLE_PANEL_ID, "Log", true); m_SceneRendererPanel = m_PanelManager->AddPanel(PanelCategory::View, SCENE_RENDERER_PANEL_ID, "Scene Renderer", true); - - // Console panel for log messages - m_PanelManager->AddPanel(PanelCategory::View, CONSOLE_PANEL_ID, "Console", true); // Render statistics panel Ref renderStatsPanel = m_PanelManager->AddPanel(PanelCategory::View, "RenderStatsPanel", "Render Stats", true); @@ -217,6 +241,7 @@ namespace Lux { m_SceneRendererPanel.reset(); m_SceneHierarchyPanel.reset(); s_Font.reset(); + EditorResources::Shutdown(); } void EditorLayer::OnUpdate(Timestep ts) @@ -244,9 +269,14 @@ namespace Lux { m_SceneRenderer->SetViewportSize(viewportWidth, viewportHeight); } + m_EditorCamera.SetActive(m_ViewportFocused || m_ViewportHovered); + EnsureSceneRenderer(m_ActiveScene, m_ViewportSize); if (s_SceneRendererState.Renderer) + { s_SceneRendererState.Renderer->GetOptions().ShowPhysicsColliders = m_ShowPhysicsColliders; + s_SceneRendererState.Renderer->GetOptions().ShowSelectedInWireframe = (m_ViewportDisplayMode == ViewportDisplayMode::SelectedWireframe); + } m_Renderer2D->ResetStats(); @@ -256,42 +286,41 @@ namespace Lux { selectedEntity = m_SceneHierarchyPanel->GetSelectedEntity(); auto isEntitySelected = [selectedEntity](Entity entity) -> bool { return selectedEntity && entity == selectedEntity; - }; + }; switch (m_SceneState) { case SceneState::Edit: { m_EditorCamera.OnUpdate(ts); - - // Render 3D content first (static meshes, lights, skybox) + + // When SceneRenderer is ready, let it own the full visible frame and + // composite the 2D pass on top of the 3D result. Falling back to the + // old Renderer2D-only path keeps the editor usable during init. if (m_SceneRenderer && m_SceneRenderer->IsReady()) m_ActiveScene->Render3D(m_EditorCamera, m_SceneRenderer, isEntitySelected); - - // Then render 2D content (sprites, circles, text) - this blends on top - m_ActiveScene->OnUpdateEditor(ts, m_EditorCamera); + else + m_ActiveScene->OnUpdateEditor(ts, m_EditorCamera); break; } case SceneState::Simulate: { m_EditorCamera.OnUpdate(ts); - - // Render 3D content + + // Update simulation first so the rendered frame matches the latest + // physics state, then render the composed 3D + 2D scene. + m_ActiveScene->OnUpdateSimulation(ts, m_EditorCamera); if (m_SceneRenderer && m_SceneRenderer->IsReady()) m_ActiveScene->Render3D(m_EditorCamera, m_SceneRenderer, isEntitySelected); - - // Then render 2D content and update physics simulation - m_ActiveScene->OnUpdateSimulation(ts, m_EditorCamera); break; } case SceneState::Play: { - // Render 3D content using the runtime camera + // Runtime has to update before rendering so sprites, circles and text + // appear in their current positions in the main viewport. + m_ActiveScene->OnUpdateRuntime(ts); if (m_SceneRenderer && m_SceneRenderer->IsReady()) m_ActiveScene->Render3DRuntime(m_SceneRenderer); - - // Then run the full runtime update (scripts, physics, 2D rendering) - m_ActiveScene->OnUpdateRuntime(ts); break; } } @@ -313,7 +342,10 @@ namespace Lux { bool opt_fullscreen = opt_fullscreen_persistent; static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None; - ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; + GLFWwindow* nativeWindow = Application::Get().GetWindow().GetNativeWindow(); + const bool isWindowMaximized = nativeWindow && glfwGetWindowAttrib(nativeWindow, GLFW_MAXIMIZED); + + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking; if (opt_fullscreen) { ImGuiViewport* viewport = ImGui::GetMainViewport(); @@ -321,7 +353,7 @@ namespace Lux { ImGui::SetNextWindowSize(viewport->Size); ImGui::SetNextWindowViewport(viewport->ID); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, isWindowMaximized ? 0.0f : 3.0f); window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; @@ -343,6 +375,15 @@ namespace Lux { ImGuiStyle& style = ImGui::GetStyle(); const float minWinSizeX = style.WindowMinSize.x; style.WindowMinSize.x = 370.0f; + + UI_DrawTitlebar(); + if (!isWindowMaximized) + { + ImGuiEx::ScopedColour borderColour(ImGuiCol_Border, IM_COL32(50, 50, 50, 255)); + ImGuiEx::RenderWindowOuterBorders(ImGui::GetCurrentWindow()); + } + + ImGui::SetCursorPosY(m_TitlebarHeight); if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) { const ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); @@ -350,45 +391,35 @@ namespace Lux { } style.WindowMinSize.x = minWinSizeX; - if (ImGui::BeginMenuBar()) - { - if (ImGui::BeginMenu("File")) - { - if (ImGui::MenuItem("Open Project...", "Ctrl+O")) - OpenProject(); - - ImGui::Separator(); - - if (ImGui::MenuItem("New Scene", "Ctrl+N")) - NewScene(); - - if (ImGui::MenuItem("Save Scene", "Ctrl+S")) - SaveScene(); - - if (ImGui::MenuItem("Save Scene As...", "Ctrl+Shift+S")) - SaveSceneAs(); + m_PanelManager->OnImGuiRender(); - ImGui::Separator(); + if (m_ShowImGuiMetrics) + ImGui::ShowMetricsWindow(&m_ShowImGuiMetrics); + if (m_ShowImGuiStyleEditor) + ImGui::ShowStyleEditor(); + if (m_ShowAboutPopup) + ImGui::OpenPopup("About LuxEngine"); - if (ImGui::MenuItem("Exit")) - Application::Get().Close(); + ImGuiViewport* mainViewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(mainViewport->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::EndMenu(); - } + if (ImGui::BeginPopupModal("About LuxEngine", &m_ShowAboutPopup, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("LuxEngine Editor"); + ImGui::Separator(); + ImGui::Text("Version: %s", Application::GetConfigurationName()); + ImGui::Text("Platform: %s", Application::GetPlatformName()); + ImGui::TextWrapped("Credits: Inspired by Hazel architecture and editor workflows."); - if (ImGui::BeginMenu("Script")) + if (ImGui::Button("Close")) { - if (ImGui::MenuItem("Reload assembly", "Ctrl+R")) - ScriptEngine::ReloadAssembly(); - - ImGui::EndMenu(); + m_ShowAboutPopup = false; + ImGui::CloseCurrentPopup(); } - ImGui::EndMenuBar(); + ImGui::EndPopup(); } - m_PanelManager->OnImGuiRender(); - // Stats panel ImGui::Begin("Stats"); @@ -447,7 +478,11 @@ namespace Lux { if (viewportPanelSize.x > 1.0f && viewportPanelSize.y > 1.0f) m_ViewportSize = { viewportPanelSize.x, viewportPanelSize.y }; - ImTextureID texID = GetImGuiTextureID(m_Framebuffer->GetImage(0)); + Ref viewportImage = m_Framebuffer->GetImage(0); + if (m_SceneRenderer && m_SceneRenderer->GetFinalPassImage()) + viewportImage = m_SceneRenderer->GetFinalPassImage(); + + ImTextureID texID = GetImGuiTextureID(viewportImage); ImGui::Image(texID, ImVec2{ m_ViewportSize.x, m_ViewportSize.y }, ImVec2{ 0, 0 }, ImVec2{ 1, 1 }); if (ImGui::BeginDragDropTarget()) @@ -460,6 +495,11 @@ namespace Lux { ImGui::EndDragDropTarget(); } + if (m_ViewportHovered) + m_HoveredEntity = CastMousePick(); + else + m_HoveredEntity = {}; + // Gizmos Entity selectedEntity = {}; if (m_SceneHierarchyPanel) @@ -480,8 +520,9 @@ namespace Lux { auto& tc = selectedEntity.GetComponent(); glm::mat4 transform = tc.GetTransform(); - bool snap = Input::IsKeyPressed(Key::LeftControl); - float snapValue = (m_GizmoType == ImGuizmo::OPERATION::ROTATE) ? 45.0f : 0.5f; + const bool controlSnap = Input::IsKeyPressed(Key::LeftControl) || Input::IsKeyPressed(Key::RightControl); + bool snap = m_UseGizmoSnap || controlSnap; + float snapValue = (m_GizmoType == ImGuizmo::OPERATION::ROTATE) ? m_RotationSnapValue : m_TranslationSnapValue; float snapValues[3] = { snapValue, snapValue, snapValue }; // FIX: Actually call Manipulate() - it was completely missing, @@ -516,111 +557,362 @@ namespace Lux { ImGui::End(); // Viewport ImGui::PopStyleVar(); - UI_Toolbar(); + UI_GizmosToolbar(); + UI_CentralToolbar(); + UI_ViewportSettings(); } ImGui::End(); // Lux Editor } - void EditorLayer::UI_Toolbar() + void EditorLayer::UI_DrawMenubar() { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0, 0)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - auto& colors = ImGui::GetStyle().Colors; - const auto& buttonHovered = colors[ImGuiCol_ButtonHovered]; - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(buttonHovered.x, buttonHovered.y, buttonHovered.z, 0.5f)); - const auto& buttonActive = colors[ImGuiCol_ButtonActive]; - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(buttonActive.x, buttonActive.y, buttonActive.z, 0.5f)); + const ImRect menuBarRect = { ImGui::GetCursorPos(), { ImGui::GetContentRegionAvail().x + ImGui::GetCursorScreenPos().x, ImGui::GetFrameHeightWithSpacing() } }; - ImGui::Begin("##toolbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::BeginGroup(); - bool toolbarEnabled = (bool)m_ActiveScene; + ImGuiEx::ScopedColourStack menuColors( + ImGuiCol_Header, ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent), + ImGuiCol_HeaderHovered, ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent), + ImGuiCol_HeaderActive, ImGui::ColorConvertU32ToFloat4(Colors::Theme::accent), + ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Colors::Theme::text)); - ImVec4 tintColor = ImVec4(1, 1, 1, 1); - if (!toolbarEnabled) - tintColor.w = 0.5f; + if (ImGuiEx::BeginMenuBar(menuBarRect)) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Create Project")) + NewProject(); + if (ImGui::MenuItem("Open Project...", "Ctrl+O")) + OpenProject(); - float size = ImGui::GetWindowHeight() - 4.0f; - ImGui::SetCursorPosX((ImGui::GetWindowContentRegionMax().x * 0.5f) - (size * 0.5f)); + if (ImGui::BeginMenu("Recent Projects")) + { + ImGui::MenuItem("No recent projects", nullptr, false, false); + ImGui::EndMenu(); + } - bool hasPlayButton = m_SceneState == SceneState::Edit || m_SceneState == SceneState::Play; - bool hasSimulateButton = m_SceneState == SceneState::Edit || m_SceneState == SceneState::Simulate; - bool hasPauseButton = m_SceneState != SceneState::Edit; + ImGui::Separator(); + if (ImGui::MenuItem("Save Scene", "Ctrl+S")) + SaveScene(); + if (ImGui::MenuItem("Save Scene As...", "Ctrl+Shift+S")) + SaveSceneAs(); - if (hasPlayButton) - { - Ref icon = (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Simulate) ? m_IconPlay : m_IconStop; - if (ImGui::ImageButton("##play", GetImGuiTextureID(icon), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1), ImVec4(0,0,0,0), tintColor)) - { - if (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Simulate) - OnScenePlay(); - else if (m_SceneState == SceneState::Play) - OnSceneStop(); + ImGui::Separator(); + if (ImGui::MenuItem("Exit")) + Application::Get().DispatchEvent(); + + ImGui::EndMenu(); } - } - if (hasSimulateButton) - { - if (hasPlayButton) - ImGui::SameLine(); + if (ImGui::BeginMenu("Edit")) + { + if (ImGui::MenuItem("Reload C# Assembly", "Ctrl+R")) + ScriptEngine::ReloadAssembly(); + ImGui::MenuItem("Second Viewport", nullptr, &m_SecondViewportEnabled); + ImGui::EndMenu(); + } - Ref icon = (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Play) ? m_IconSimulate : m_IconStop; - if (ImGui::ImageButton("##simulate", GetImGuiTextureID(icon), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1), ImVec4(0,0,0,0), tintColor)) + if (ImGui::BeginMenu("View")) { - if (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Play) - OnSceneSimulate(); - else if (m_SceneState == SceneState::Simulate) - OnSceneStop(); + auto& viewPanels = m_PanelManager->GetPanels(PanelCategory::View); + for (auto& [id, panelData] : viewPanels) + ImGui::MenuItem(panelData.Name, nullptr, &panelData.IsOpen); + ImGui::EndMenu(); } - } - if (hasPauseButton) - { - bool isPaused = m_ActiveScene->IsPaused(); - ImGui::SameLine(); + if (ImGui::BeginMenu("Tools")) { - if (ImGui::ImageButton("##pause", GetImGuiTextureID(m_IconPause), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1), ImVec4(0,0,0,0), tintColor) && toolbarEnabled) - m_ActiveScene->SetPaused(!isPaused); + ImGui::MenuItem("ImGui Metrics", nullptr, &m_ShowImGuiMetrics); + ImGui::MenuItem("ImGui Style Editor", nullptr, &m_ShowImGuiStyleEditor); + ImGui::EndMenu(); } - if (isPaused) + if (ImGui::BeginMenu("Help")) { - ImGui::SameLine(); - if (ImGui::ImageButton("##step", GetImGuiTextureID(m_IconStep), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1), ImVec4(0,0,0,0), tintColor) && toolbarEnabled) - m_ActiveScene->Step(); + if (ImGui::MenuItem("About")) + m_ShowAboutPopup = true; + ImGui::EndMenu(); } + + ImGuiEx::EndMenuBar(); + } + + ImGui::EndGroup(); + } + + void EditorLayer::UI_DrawTitlebar() + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 windowPos = window->Pos; + const ImVec2 windowMax = ImVec2(window->Pos.x + window->Size.x, window->Pos.y + m_TitlebarHeight); + + ImU32 targetTitlebarColor = Colors::Theme::titlebar; + if (m_SceneState == SceneState::Play) + targetTitlebarColor = Colors::Theme::titlebarOrange; + else if (m_SceneState == SceneState::Simulate) + targetTitlebarColor = Colors::Theme::titlebarGreen; + + const ImVec4 targetColor = ImGui::ColorConvertU32ToFloat4(targetTitlebarColor); + const float dt = ImGui::GetIO().DeltaTime; + m_AnimatedTitlebarColor = ImLerp(m_AnimatedTitlebarColor, targetColor, std::clamp(dt * 8.0f, 0.0f, 1.0f)); + drawList->AddRectFilled(windowPos, windowMax, ImGui::ColorConvertFloat4ToU32(m_AnimatedTitlebarColor)); + + drawList->AddLine(ImVec2(windowPos.x, windowPos.y + m_TitlebarHeight), ImVec2(windowPos.x + window->Size.x, windowPos.y + m_TitlebarHeight), Colors::Theme::backgroundDark); + + const float logoPadding = 16.0f; + const float logoSize = 30.0f; + const float logoTop = (m_TitlebarHeight - logoSize) * 0.5f; + const ImVec2 logoMin(windowPos.x + logoPadding, windowPos.y + logoTop); + const ImVec2 logoMax(logoMin.x + logoSize, logoMin.y + logoSize); + if (EditorResources::HazelLogoTexture) + drawList->AddImage(GetImGuiTextureID(EditorResources::HazelLogoTexture), logoMin, logoMax); + + const float menuBarX = logoPadding * 2.0f + logoSize + 8.0f; + ImGui::SetCursorPos(ImVec2(menuBarX, 4.0f)); + UI_DrawMenubar(); + + const std::string sceneName = GetSceneDisplayName(m_EditorScenePath); + const ImVec2 sceneNameSize = ImGui::CalcTextSize(sceneName.c_str()); + const float sceneNameX = windowPos.x + (window->Size.x - sceneNameSize.x) * 0.5f; + const float sceneNameY = windowPos.y + (m_TitlebarHeight - sceneNameSize.y) * 0.5f; + drawList->AddText(ImVec2(sceneNameX, sceneNameY), Colors::Theme::textBrighter, sceneName.c_str()); + drawList->AddLine( + ImVec2(sceneNameX - 6.0f, sceneNameY + sceneNameSize.y + 4.0f), + ImVec2(sceneNameX + sceneNameSize.x + 6.0f, sceneNameY + sceneNameSize.y + 4.0f), + Colors::Theme::accent, 1.5f); + + GLFWwindow* nativeWindow = Application::Get().GetWindow().GetNativeWindow(); + const bool isMaximized = nativeWindow && glfwGetWindowAttrib(nativeWindow, GLFW_MAXIMIZED); + + const float controlsWidth = 120.0f; + const float dragZoneMinX = 70.0f; + const float dragZoneMaxX = window->Size.x - controlsWidth - 220.0f; + ImGui::SetCursorPos(ImVec2(dragZoneMinX, 0.0f)); + ImGui::InvisibleButton("##titleBarDragZone", ImVec2(std::max(0.0f, dragZoneMaxX - dragZoneMinX), m_TitlebarHeight)); + const ImVec2 dragMin = ImGui::GetItemRectMin(); + const ImVec2 dragMax = ImGui::GetItemRectMax(); + m_TitleBarDragRectMin = ImVec2(dragMin.x - windowPos.x, dragMin.y - windowPos.y); + m_TitleBarDragRectMax = ImVec2(dragMax.x - windowPos.x, dragMax.y - windowPos.y); + +#if !defined(LUX_PLATFORM_WINDOWS) + if (nativeWindow && !isMaximized && ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + { + int windowX = 0, windowY = 0; + glfwGetWindowPos(nativeWindow, &windowX, &windowY); + const ImVec2 delta = ImGui::GetIO().MouseDelta; + glfwSetWindowPos(nativeWindow, windowX + (int)delta.x, windowY + (int)delta.y); } +#endif + + const std::string projectName = GetProjectDisplayName(); + const ImVec2 projectNameSize = ImGui::CalcTextSize(projectName.c_str()); + const ImVec2 projectBoxMin(windowPos.x + window->Size.x - controlsWidth - projectNameSize.x - 36.0f, windowPos.y + 14.0f); + const ImVec2 projectBoxMax(projectBoxMin.x + projectNameSize.x + 20.0f, projectBoxMin.y + 26.0f); + drawList->AddRect(projectBoxMin, projectBoxMax, Colors::Theme::muted, 6.0f, 0, 1.0f); + drawList->AddText(ImVec2(projectBoxMin.x + 10.0f, projectBoxMin.y + 5.0f), Colors::Theme::text, projectName.c_str()); + + const float buttonSize = 34.0f; + const float buttonY = (m_TitlebarHeight - buttonSize) * 0.5f; + const float buttonsStartX = window->Size.x - controlsWidth; + const ImU32 normalTint = IM_COL32(220, 220, 220, 220); + const ImU32 hoverTint = IM_COL32(255, 255, 255, 255); + const ImU32 activeTint = IM_COL32(200, 200, 200, 255); + + auto drawWindowControlButton = [&](const char* id, const Ref& icon, float localX, auto&& onClick) + { + ImGui::SetCursorPos(ImVec2(localX, buttonY)); + ImGui::InvisibleButton(id, ImVec2(buttonSize, buttonSize)); + if (icon) + ImGuiEx::DrawButtonImage(icon, normalTint, hoverTint, activeTint, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f)); + + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + onClick(); + }; + + drawWindowControlButton("##minimizeWindow", EditorResources::MinimizeIcon, buttonsStartX + 4.0f, [nativeWindow]() + { + if (!nativeWindow) + return; + Application::Get().QueueEvent([nativeWindow]() { glfwIconifyWindow(nativeWindow); }); + }); + + drawWindowControlButton("##maximizeRestoreWindow", isMaximized ? EditorResources::RestoreIcon : EditorResources::MaximizeIcon, buttonsStartX + 42.0f, [nativeWindow, isMaximized]() + { + if (!nativeWindow) + return; + Application::Get().QueueEvent([nativeWindow, isMaximized]() + { + if (isMaximized) + glfwRestoreWindow(nativeWindow); + else + glfwMaximizeWindow(nativeWindow); + }); + }); + + drawWindowControlButton("##closeWindow", EditorResources::CloseIcon, buttonsStartX + 80.0f, []() + { + Application::Get().DispatchEvent(); + }); + } + + void EditorLayer::UI_GizmosToolbar() + { + if (m_ViewportSize.x <= 0.0f || m_ViewportSize.y <= 0.0f) + return; + + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + + ImGui::SetNextWindowPos(ImVec2(m_ViewportBounds[0].x + 12.0f, m_ViewportBounds[0].y + 12.0f), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.55f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 4.0f)); + ImGui::Begin("##viewport_gizmos_toolbar", nullptr, flags); + + const ImU32 normalTint = IM_COL32(215, 215, 215, 220); + const ImU32 hoverTint = IM_COL32(255, 255, 255, 255); + const ImU32 activeTint = IM_COL32(235, 235, 235, 255); + const ImVec2 buttonSize(24.0f, 24.0f); + auto gizmoButton = [&](const char* id, Ref icon, int gizmoMode) + { + ImGui::InvisibleButton(id, buttonSize); + ImGuiEx::DrawButtonImage(icon, normalTint, hoverTint, activeTint, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f)); + + if (m_GizmoType == gizmoMode) + ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), Colors::Theme::accent, 2.0f, 0, 2.0f); + + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + m_GizmoType = gizmoMode; + }; + + gizmoButton("##gizmo_select", EditorResources::PointerIcon, -1); + ImGui::SameLine(); + gizmoButton("##gizmo_translate", EditorResources::MoveIcon, ImGuizmo::TRANSLATE); ImGui::SameLine(); - ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + gizmoButton("##gizmo_rotate", EditorResources::RotateIcon, ImGuizmo::ROTATE); ImGui::SameLine(); + gizmoButton("##gizmo_scale", EditorResources::ScaleIcon, ImGuizmo::SCALE); + + ImGui::End(); + ImGui::PopStyleVar(2); + } + + void EditorLayer::UI_CentralToolbar() + { + if (m_ViewportSize.x <= 0.0f || m_ViewportSize.y <= 0.0f) + return; + + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; - // Gizmo mode indicator - const char* gizmoMode = "None"; - if (m_GizmoType == ImGuizmo::TRANSLATE) gizmoMode = "Translate"; - else if (m_GizmoType == ImGuizmo::ROTATE) gizmoMode = "Rotate"; - else if (m_GizmoType == ImGuizmo::SCALE) gizmoMode = "Scale"; + const float toolbarWidth = 118.0f; + const float posX = m_ViewportBounds[0].x + (m_ViewportSize.x - toolbarWidth) * 0.5f; + const float posY = m_ViewportBounds[0].y + 12.0f; - ImGui::Text("%s", gizmoMode); + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.55f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 0.0f)); + ImGui::Begin("##viewport_central_toolbar", nullptr, flags); + + const ImU32 normalTint = IM_COL32(215, 215, 215, 220); + const ImU32 hoverTint = IM_COL32(255, 255, 255, 255); + const ImU32 activeTint = IM_COL32(235, 235, 235, 255); + const ImVec2 buttonSize(24.0f, 24.0f); + + auto controlButton = [&](const char* id, Ref icon, bool active, const std::function& onClick) + { + ImGui::InvisibleButton(id, buttonSize); + ImGuiEx::DrawButtonImage(icon, normalTint, hoverTint, activeTint, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f)); + if (active) + ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), Colors::Theme::accent, 2.0f, 0, 2.0f); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + onClick(); + }; + + controlButton("##scene_play", EditorResources::PlayIcon, m_SceneState == SceneState::Play, [this]() + { + if (m_SceneState != SceneState::Play) + OnScenePlay(); + }); + ImGui::SameLine(); + controlButton("##scene_simulate", EditorResources::SimulateIcon, m_SceneState == SceneState::Simulate, [this]() + { + if (m_SceneState != SceneState::Simulate) + OnSceneSimulate(); + }); ImGui::SameLine(); + controlButton("##scene_stop", EditorResources::StopIcon, m_SceneState == SceneState::Edit, [this]() + { + if (m_SceneState != SceneState::Edit) + OnSceneStop(); + }); - // Grid toggle - if (s_SceneRendererState.Renderer) + ImGui::End(); + ImGui::PopStyleVar(2); + } + + void EditorLayer::UI_ViewportSettings() + { + if (m_ViewportSize.x <= 0.0f || m_ViewportSize.y <= 0.0f) + return; + + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + + ImGui::SetNextWindowPos(ImVec2(m_ViewportBounds[1].x - 44.0f, m_ViewportBounds[0].y + 12.0f), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.55f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f)); + ImGui::Begin("##viewport_settings_toolbar", nullptr, flags); + + const ImVec2 buttonSize(24.0f, 24.0f); + ImGui::InvisibleButton("##viewport_settings_btn", buttonSize); + ImGuiEx::DrawButtonImage(EditorResources::GearIcon, IM_COL32(215, 215, 215, 220), IM_COL32(255, 255, 255, 255), IM_COL32(235, 235, 235, 255), ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f)); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + ImGui::OpenPopup("##viewport_settings_popup"); + + if (ImGui::BeginPopup("##viewport_settings_popup")) { - bool showGrid = s_SceneRendererState.Renderer->GetOptions().ShowGrid; - ImGui::SameLine(); - if (ImGui::Checkbox("##grid", &showGrid)) + ImGui::Checkbox("Enable Gizmo Snap", &m_UseGizmoSnap); + if (m_UseGizmoSnap) + { + ImGui::DragFloat("Translate Snap", &m_TranslationSnapValue, 0.05f, 0.05f, 10.0f, "%.2f"); + ImGui::DragFloat("Rotate Snap", &m_RotationSnapValue, 1.0f, 1.0f, 180.0f, "%.0f"); + } + + ImGui::Separator(); + ImGui::Checkbox("Show Bounding Boxes", &m_ShowBoundingBoxes); + ImGui::Checkbox("Show Entity Icons", &m_ShowEntityIcons); + + if (s_SceneRendererState.Renderer) { - s_SceneRendererState.Renderer->GetOptions().ShowGrid = showGrid; + auto& options = s_SceneRendererState.Renderer->GetOptions(); + ImGui::Checkbox("Show Grid", &options.ShowGrid); + ImGui::Checkbox("Show Physics Colliders", &options.ShowPhysicsColliders); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Toggle Grid"); + + int displayMode = (int)m_ViewportDisplayMode; + if (ImGui::Combo("Display Mode", &displayMode, "Lit\0Selected Wireframe\0")) + m_ViewportDisplayMode = (ViewportDisplayMode)displayMode; + + ImGui::EndPopup(); } - ImGui::PopStyleVar(2); - ImGui::PopStyleColor(3); ImGui::End(); + ImGui::PopStyleVar(); + } + + void EditorLayer::UI_Toolbar() + { + UI_GizmosToolbar(); + UI_CentralToolbar(); + UI_ViewportSettings(); } void EditorLayer::OnEvent(Event& e) @@ -633,6 +925,7 @@ namespace Lux { EventDispatcher dispatcher(e); dispatcher.Dispatch(LUX_BIND_EVENT_FN(EditorLayer::OnKeyPressed)); dispatcher.Dispatch(LUX_BIND_EVENT_FN(EditorLayer::OnMouseButtonPressed)); + dispatcher.Dispatch(LUX_BIND_EVENT_FN(EditorLayer::OnTitleBarHitTest)); } bool EditorLayer::OnKeyPressed(KeyPressedEvent& e) @@ -672,14 +965,177 @@ namespace Lux { if (e.GetMouseButton() == MouseButton::Left) { if (m_ViewportHovered && !ImGuizmo::IsOver() && !Input::IsKeyPressed(Key::LeftAlt)) + { + m_HoveredEntity = CastMousePick(); if (m_SceneHierarchyPanel) m_SceneHierarchyPanel->SetSelectedEntity(m_HoveredEntity); + } } return false; } + Entity EditorLayer::CastMousePick() + { + if (!m_ActiveScene) + return {}; + + const float viewportWidth = m_ViewportBounds[1].x - m_ViewportBounds[0].x; + const float viewportHeight = m_ViewportBounds[1].y - m_ViewportBounds[0].y; + if (viewportWidth <= 1.0f || viewportHeight <= 1.0f) + return {}; + + const float mouseX = Input::GetMouseX() - m_ViewportBounds[0].x; + const float mouseY = Input::GetMouseY() - m_ViewportBounds[0].y; + if (mouseX < 0.0f || mouseY < 0.0f || mouseX > viewportWidth || mouseY > viewportHeight) + return {}; + + const float ndcX = (mouseX / viewportWidth) * 2.0f - 1.0f; + const float ndcY = 1.0f - (mouseY / viewportHeight) * 2.0f; + + glm::mat4 inverseViewProjection = glm::inverse(m_EditorCamera.GetUnReversedViewProjection()); + glm::vec4 nearPoint = inverseViewProjection * glm::vec4(ndcX, ndcY, 0.0f, 1.0f); + glm::vec4 farPoint = inverseViewProjection * glm::vec4(ndcX, ndcY, 1.0f, 1.0f); + if (nearPoint.w == 0.0f || farPoint.w == 0.0f) + return {}; + + nearPoint /= nearPoint.w; + farPoint /= farPoint.w; + + const glm::vec3 rayOrigin = glm::vec3(nearPoint); + const glm::vec3 rayVector = glm::vec3(farPoint - nearPoint); + if (glm::dot(rayVector, rayVector) <= std::numeric_limits::epsilon()) + return {}; + const glm::vec3 rayDirection = glm::normalize(rayVector); + + Entity closestEntity = {}; + float closestDistance = std::numeric_limits::max(); + + auto view = m_ActiveScene->GetAllEntitiesWith(); + for (auto entityID : view) + { + Entity entity(entityID, m_ActiveScene.Raw()); + float distance = 0.0f; + if (!RayIntersectsEntity(entity, rayOrigin, rayDirection, distance)) + continue; + + if (distance < closestDistance) + { + closestDistance = distance; + closestEntity = entity; + } + } + + return closestEntity; + } + + bool EditorLayer::RayIntersectsEntity(Entity entity, const glm::vec3& rayOrigin, const glm::vec3& rayDirection, float& outDistance) const + { + outDistance = std::numeric_limits::max(); + if (!entity || !entity.HasComponent()) + return false; + + const TransformComponent& transformComponent = entity.GetComponent(); + const glm::mat4 worldTransform = transformComponent.GetTransform(); + bool hit = false; + + auto testAABB = [&](const AABB& localAABB, const glm::mat4& localTransform = glm::mat4(1.0f)) + { + const glm::mat4 inverseTransform = glm::inverse(worldTransform * localTransform); + const glm::vec3 localOrigin = glm::vec3(inverseTransform * glm::vec4(rayOrigin, 1.0f)); + const glm::vec3 localDirectionVector = glm::vec3(inverseTransform * glm::vec4(rayDirection, 0.0f)); + if (glm::dot(localDirectionVector, localDirectionVector) <= std::numeric_limits::epsilon()) + return; + const glm::vec3 localDirection = glm::normalize(localDirectionVector); + + Ray localRay(localOrigin, localDirection); + float t = 0.0f; + if (localRay.IntersectsAABB(localAABB, t) && t >= 0.0f && t < outDistance) + { + outDistance = t; + hit = true; + } + }; + + if (entity.HasComponent()) + { + const auto& staticMeshComponent = entity.GetComponent(); + Ref staticMesh = AssetManager::GetAsset(staticMeshComponent.Mesh); + if (staticMesh) + { + Ref meshSource = AssetManager::GetAsset(staticMesh->GetMeshSource()); + if (meshSource) + testAABB(meshSource->GetBoundingBox()); + } + } + + if (entity.HasComponent()) + { + const auto& collider = entity.GetComponent(); + const glm::vec3 extents = glm::vec3(collider.Size, 0.05f); + const glm::vec3 center = glm::vec3(collider.Offset, 0.0f); + testAABB(AABB(center - extents, center + extents)); + } + + if (entity.HasComponent()) + { + const auto& collider = entity.GetComponent(); + const glm::vec3 extents = glm::vec3(collider.Radius, collider.Radius, 0.05f); + const glm::vec3 center = glm::vec3(collider.Offset, 0.0f); + testAABB(AABB(center - extents, center + extents)); + } + + const bool shouldUseIconSelection = entity.HasComponent() || + entity.HasComponent() || + entity.HasComponent() || + entity.HasComponent() || + entity.HasComponent() || + entity.HasComponent(); + + if (!hit && shouldUseIconSelection) + { + const glm::vec3 center = transformComponent.Translation; + const float radius = 0.35f; + const glm::vec3 toCenter = rayOrigin - center; + const float b = glm::dot(toCenter, rayDirection); + const float c = glm::dot(toCenter, toCenter) - radius * radius; + const float discriminant = b * b - c; + if (discriminant >= 0.0f) + { + const float t = -b - std::sqrt(discriminant); + if (t >= 0.0f && t < outDistance) + { + outDistance = t; + hit = true; + } + } + } + + return hit; + } + + bool EditorLayer::OnTitleBarHitTest(WindowTitleBarHitTestEvent& e) + { + const float x = (float)e.GetX(); + const float y = (float)e.GetY(); + + const bool inDragZone = x >= m_TitleBarDragRectMin.x && x <= m_TitleBarDragRectMax.x + && y >= m_TitleBarDragRectMin.y && y <= m_TitleBarDragRectMax.y; + + if (inDragZone) + e.SetHit(true); + + return inDragZone; + } + void EditorLayer::OnOverlayRender() { + Ref overlayTarget = m_Framebuffer; + if (m_SceneRenderer && m_SceneRenderer->GetExternalCompositeFramebuffer()) + overlayTarget = m_SceneRenderer->GetExternalCompositeFramebuffer(); + + if (overlayTarget) + m_Renderer2D->SetTargetFramebuffer(overlayTarget); + if (m_SceneState == SceneState::Play) { Entity camera = m_ActiveScene->GetPrimaryCameraEntity(); @@ -742,14 +1198,35 @@ namespace Lux { } } - // Selected entity outline - Entity selectedEntity = {}; - if (m_SceneHierarchyPanel) - selectedEntity = m_SceneHierarchyPanel->GetSelectedEntity(); - if (selectedEntity) + } + + Entity selectedEntity = {}; + if (m_SceneHierarchyPanel) + selectedEntity = m_SceneHierarchyPanel->GetSelectedEntity(); + + if (selectedEntity) + { + const TransformComponent& transform = selectedEntity.GetComponent(); + const glm::mat4 worldTransform = transform.GetTransform(); + + bool drewBoundingBox = false; + if (m_ShowBoundingBoxes && selectedEntity.HasComponent()) + { + const auto& smc = selectedEntity.GetComponent(); + Ref staticMesh = AssetManager::GetAsset(smc.Mesh); + if (staticMesh) + { + Ref meshSource = AssetManager::GetAsset(staticMesh->GetMeshSource()); + if (meshSource) + { + m_Renderer2D->DrawAABB(meshSource->GetBoundingBox(), worldTransform, glm::vec4(1.0f, 0.5f, 0.0f, 1.0f), true); + drewBoundingBox = true; + } + } + } + + if (!drewBoundingBox) { - const TransformComponent& transform = selectedEntity.GetComponent(); - glm::mat4 t = transform.GetTransform(); glm::vec4 color(1.0f, 0.5f, 0.0f, 1.0f); glm::vec4 corners[4] = { {-0.5f, -0.5f, 0.0f, 1.0f}, { 0.5f, -0.5f, 0.0f, 1.0f}, @@ -757,13 +1234,35 @@ namespace Lux { }; for (int i = 0; i < 4; i++) { - glm::vec3 p0 = t * corners[i]; - glm::vec3 p1 = t * corners[(i + 1) % 4]; + glm::vec3 p0 = worldTransform * corners[i]; + glm::vec3 p1 = worldTransform * corners[(i + 1) % 4]; m_Renderer2D->DrawLine(p0, p1, color); } } } + if (m_ShowEntityIcons) + { + auto drawIconForView = [this](auto view, const Ref& iconTexture) + { + if (!iconTexture) + return; + + for (auto entityID : view) + { + auto& transform = view.template get(entityID); + m_Renderer2D->DrawQuadBillboard(transform.Translation, glm::vec2(0.35f), iconTexture, 1.0f, glm::vec4(1.0f)); + } + }; + + drawIconForView(m_ActiveScene->GetAllEntitiesWith(), EditorResources::CameraIcon); + drawIconForView(m_ActiveScene->GetAllEntitiesWith(), EditorResources::AudioIcon); + drawIconForView(m_ActiveScene->GetAllEntitiesWith(), EditorResources::AudioListenerIcon); + drawIconForView(m_ActiveScene->GetAllEntitiesWith(), EditorResources::DirectionalLightIcon); + drawIconForView(m_ActiveScene->GetAllEntitiesWith(), EditorResources::PointLightIcon); + drawIconForView(m_ActiveScene->GetAllEntitiesWith(), EditorResources::SpotLightIcon); + } + m_Renderer2D->EndScene(); } diff --git a/Editor/Source/EditorLayer.h b/Editor/Source/EditorLayer.h index a17041b..049af23 100644 --- a/Editor/Source/EditorLayer.h +++ b/Editor/Source/EditorLayer.h @@ -2,15 +2,16 @@ #include "Lux.h" -#include "Panels/ConsolePanel.h" #include "Panels/RenderStatsPanel.h" #include "Panels/MaterialEditorPanel.h" #include "Panels/LightSettingsPanel.h" +#include "Lux/Editor/EditorConsolePanel.h" #include "Lux/Asset/Asset.h" #include "Lux/Scene/Entity.h" #include "Lux/Scene/Scene.h" #include "Lux/Editor/EditorCamera.h" +#include "Lux/ImGui/ImGuiEx.h" #include "Lux/Renderer/Renderer2D.h" #include "Lux/Renderer/Framebuffer.h" #include "Lux/Renderer/Shader.h" @@ -40,9 +41,17 @@ namespace Lux private: bool OnKeyPressed(KeyPressedEvent& e); bool OnMouseButtonPressed(MouseButtonPressedEvent& e); + bool OnTitleBarHitTest(WindowTitleBarHitTestEvent& e); //bool OnWindowDrop(WindowDropEvent& e); void OnOverlayRender(); + void UI_DrawTitlebar(); + void UI_DrawMenubar(); + void UI_GizmosToolbar(); + void UI_CentralToolbar(); + void UI_ViewportSettings(); + Entity CastMousePick(); + bool RayIntersectsEntity(Entity entity, const glm::vec3& rayOrigin, const glm::vec3& rayDirection, float& outDistance) const; void NewProject(); bool OpenProject(); @@ -79,6 +88,7 @@ namespace Lux Ref m_SceneRenderer; Ref m_SceneHierarchyPanel; Ref m_SceneRendererPanel; + Ref m_ConsolePanel; // Temp Ref m_SquareVA; @@ -98,7 +108,7 @@ namespace Lux Ref m_CheckerboardTexture; - glm::vec2 m_ViewportSize = {0.0f, 0.0f}; + glm::vec2 m_ViewportSize = { 0.0f, 0.0f }; glm::vec2 m_ViewportBounds[2]; glm::vec4 m_SquareColor = { 0.2f, 0.3f, 0.8f, 1.0f }; @@ -106,6 +116,11 @@ namespace Lux int m_GizmoType = -1; bool m_ShowPhysicsColliders = false; + bool m_ShowBoundingBoxes = false; + bool m_ShowEntityIcons = true; + bool m_UseGizmoSnap = false; + float m_TranslationSnapValue = 0.5f; + float m_RotationSnapValue = 45.0f; enum class SceneState { @@ -113,6 +128,23 @@ namespace Lux }; SceneState m_SceneState = SceneState::Edit; + enum class ViewportDisplayMode + { + Lit = 0, + SelectedWireframe = 1 + }; + ViewportDisplayMode m_ViewportDisplayMode = ViewportDisplayMode::Lit; + + ImVec4 m_AnimatedTitlebarColor = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); + ImVec2 m_TitleBarDragRectMin = { 0.0f, 0.0f }; + ImVec2 m_TitleBarDragRectMax = { 0.0f, 0.0f }; + float m_TitlebarHeight = 57.0f; + + bool m_ShowImGuiMetrics = false; + bool m_ShowImGuiStyleEditor = false; + bool m_ShowAboutPopup = false; + bool m_SecondViewportEnabled = false; + // Editor resources Ref m_IconPlay, m_IconPause, m_IconStep, m_IconSimulate, m_IconStop; }; diff --git a/Editor/Source/Panels/ConsolePanel.cpp b/Editor/Source/Panels/ConsolePanel.cpp deleted file mode 100644 index a598ea7..0000000 --- a/Editor/Source/Panels/ConsolePanel.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "lpch.h" -#include "ConsolePanel.h" - -#include -#include "Lux/ImGui/ImGuiEx.h" - -namespace Lux { - - ConsolePanel::ConsolePanel() - { - // Register the logging callback - Log::SetConsoleCallback([this](uint8_t level, const std::string& message) { - OnLogMessage(level, message); - }); - } - - ConsolePanel::~ConsolePanel() - { - // Clear the callback to prevent dangling pointer - Log::ClearConsoleCallback(); - } - - void ConsolePanel::OnLogMessage(uint8_t level, const std::string& message) - { - std::lock_guard lock(m_MessageMutex); - - LogMessage msg; - msg.Level = static_cast(level); - msg.Message = message; - - m_Messages.push_back(std::move(msg)); - - // Trim to max size - while (m_Messages.size() > MaxMessages) - m_Messages.pop_front(); - } - - void ConsolePanel::Clear() - { - std::lock_guard lock(m_MessageMutex); - m_Messages.clear(); - } - - void ConsolePanel::OnImGuiRender(bool& isOpen) - { - if (!isOpen) - return; - - ImGui::Begin("Console", &isOpen); - - // Toolbar - if (ImGui::Button("Clear")) - Clear(); - - ImGuiEx::BeginPropertyGrid(); - ImGuiEx::Property("Auto-scroll", m_AutoScroll); - ImGuiEx::Property("Trace", m_ShowTrace); - ImGuiEx::Property("Info", m_ShowInfo); - ImGuiEx::Property("Warn", m_ShowWarn); - ImGuiEx::Property("Error", m_ShowError); - ImGuiEx::EndPropertyGrid(); - ImGui::Separator(); - - // Filter input - ImGui::Text("Filter:"); - ImGui::SameLine(); - ImGui::InputText("##filter", m_FilterBuffer, sizeof(m_FilterBuffer)); - - ImGui::Separator(); - - // Messages list - ImGui::BeginChild("ScrollingRegion", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); - - { - std::lock_guard lock(m_MessageMutex); - - for (const auto& msg : m_Messages) - { - // Filter by level - switch (msg.Level) - { - case Log::Level::Trace: - if (!m_ShowTrace) continue; - break; - case Log::Level::Info: - if (!m_ShowInfo) continue; - break; - case Log::Level::Warn: - if (!m_ShowWarn) continue; - break; - case Log::Level::Error: - case Log::Level::Fatal: - if (!m_ShowError) continue; - break; - } - - // Filter by text - if (m_FilterBuffer[0] != '\0') - { - if (msg.Message.find(m_FilterBuffer) == std::string::npos) - continue; - } - - // Set color based on level - ImVec4 color; - switch (msg.Level) - { - case Log::Level::Trace: - color = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); - break; - case Log::Level::Info: - color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f); - break; - case Log::Level::Warn: - color = ImVec4(1.0f, 0.8f, 0.0f, 1.0f); - break; - case Log::Level::Error: - color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); - break; - case Log::Level::Fatal: - color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); - break; - default: - color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - break; - } - - ImGui::PushStyleColor(ImGuiCol_Text, color); - ImGui::TextUnformatted(msg.Message.c_str()); - ImGui::PopStyleColor(); - } - } - - // Auto-scroll - if (m_AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) - ImGui::SetScrollHereY(1.0f); - - ImGui::EndChild(); - - ImGui::End(); - } - -} diff --git a/Editor/Source/Panels/ConsolePanel.h b/Editor/Source/Panels/ConsolePanel.h deleted file mode 100644 index 1050164..0000000 --- a/Editor/Source/Panels/ConsolePanel.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include "Lux/Editor/EditorPanel.h" -#include "Lux/Core/Log.h" - -#include -#include -#include - -namespace Lux { - - class ConsolePanel : public EditorPanel - { - public: - ConsolePanel(); - virtual ~ConsolePanel(); - - virtual void OnImGuiRender(bool& isOpen) override; - - void Clear(); - - private: - void OnLogMessage(uint8_t level, const std::string& message); - - struct LogMessage - { - Log::Level Level; - std::string Message; - }; - - static constexpr size_t MaxMessages = 1000; - - std::deque m_Messages; - std::mutex m_MessageMutex; - - bool m_AutoScroll = true; - bool m_ShowTrace = true; - bool m_ShowInfo = true; - bool m_ShowWarn = true; - bool m_ShowError = true; - - // Filter text - char m_FilterBuffer[256] = ""; - }; - -} diff --git a/Editor/Source/Panels/SceneHierarchyPanel.cpp b/Editor/Source/Panels/SceneHierarchyPanel.cpp index 798f5e5..c601fcf 100644 --- a/Editor/Source/Panels/SceneHierarchyPanel.cpp +++ b/Editor/Source/Panels/SceneHierarchyPanel.cpp @@ -2,9 +2,9 @@ #include "SceneHierarchyPanel.h" #include "Lux/Scene/Components.h" +#include "Lux/Scene/Prefab.h" #include "Lux/Scripting/ScriptEngine.h" -#include "Lux/UI/UI.h" #include "Lux/ImGui/ImGuiEx.h" #include "Lux/Asset/AssetManager.h" @@ -50,14 +50,48 @@ namespace Lux { return; } - if (m_Context) - { - m_Context->m_Registry.each([&](auto entityID) + if (m_Context) { - Entity entity{ entityID , m_Context.get() }; - DrawEntityNode(entity); - }); - } + m_Context->m_Registry.each([&](auto entityID) + { + Entity entity{ entityID , m_Context.get() }; + if (!entity.HasComponent()) + { + DrawEntityNode(entity); + return; + } + + const auto& relationship = entity.GetComponent(); + const bool hasValidParent = relationship.ParentHandle != 0 && (bool)m_Context->GetEntityByUUID(relationship.ParentHandle); + if (!hasValidParent) + DrawEntityNode(entity); + }); + + const ImRect hierarchyRect(ImGui::GetWindowPos(), ImGui::GetWindowPos() + ImGui::GetWindowSize()); + if (ImGui::BeginDragDropTargetCustom(hierarchyRect, ImGui::GetID("SceneHierarchyRootTarget"))) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_HIERARCHY_ENTITY")) + { + const UUID draggedEntityID = *(const UUID*)payload->Data; + Entity draggedEntity = m_Context->GetEntityByUUID(draggedEntityID); + if (draggedEntity) + draggedEntity.SetParent({}); + } + + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_ITEM")) + { + AssetHandle handle = *(AssetHandle*)payload->Data; + if (AssetManager::GetAssetType(handle) == AssetType::Prefab) + { + Ref prefab = AssetManager::GetAsset(handle); + if (prefab) + m_SelectionContext = m_Context->InstantiatePrefab(prefab); + } + } + + ImGui::EndDragDropTarget(); + } + } if (ImGui::IsMouseDown(0) && ImGui::IsWindowHovered()) m_SelectionContext = {}; @@ -98,15 +132,58 @@ namespace Lux { void SceneHierarchyPanel::DrawEntityNode(Entity entity) { auto& tag = entity.GetComponent().Tag; + const auto& relationship = entity.GetComponent(); + const bool hasChildren = !relationship.Children.empty(); ImGuiTreeNodeFlags flags = ((m_SelectionContext == entity) ? ImGuiTreeNodeFlags_Selected : 0) | ImGuiTreeNodeFlags_OpenOnArrow; flags |= ImGuiTreeNodeFlags_SpanAvailWidth; + if (!hasChildren) + flags |= ImGuiTreeNodeFlags_Leaf; bool opened = ImGui::TreeNodeEx((void*)(uint64_t)(uint32_t)entity, flags, tag.c_str()); if (ImGui::IsItemClicked()) { m_SelectionContext = entity; } + if (ImGui::BeginDragDropSource()) + { + const UUID entityID = entity.GetUUID(); + ImGui::SetDragDropPayload("SCENE_HIERARCHY_ENTITY", &entityID, sizeof(UUID)); + ImGui::TextUnformatted(tag.c_str()); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_HIERARCHY_ENTITY")) + { + const UUID draggedEntityID = *(const UUID*)payload->Data; + Entity draggedEntity = m_Context->GetEntityByUUID(draggedEntityID); + if (draggedEntity && draggedEntity != entity) + draggedEntity.SetParent(entity); + } + + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_ITEM")) + { + AssetHandle handle = *(AssetHandle*)payload->Data; + if (AssetManager::GetAssetType(handle) == AssetType::Prefab) + { + Ref prefab = AssetManager::GetAsset(handle); + if (prefab) + { + Entity instantiated = m_Context->InstantiatePrefab(prefab); + if (instantiated) + { + instantiated.SetParent(entity); + m_SelectionContext = instantiated; + } + } + } + } + + ImGui::EndDragDropTarget(); + } + bool entityDeleted = false; if (ImGui::BeginPopupContextItem()) { @@ -118,6 +195,13 @@ namespace Lux { if (opened) { + for (UUID childID : relationship.Children) + { + Entity childEntity = m_Context->GetEntityByUUID(childID); + if (childEntity) + DrawEntityNode(childEntity); + } + ImGui::TreePop(); } @@ -406,12 +490,23 @@ namespace Lux { static char buffer[64]; strcpy_s(buffer, sizeof(buffer), component.ClassName.c_str()); - UI::ScopedStyleColor textColor(ImGuiCol_Text, ImVec4(0.9f, 0.2f, 0.3f, 1.0f), !scriptClassExists); + if (!scriptClassExists) + { + ImGuiEx::ScopedColour textColor(ImGuiCol_Text, ImVec4(0.9f, 0.2f, 0.3f, 1.0f)); - if (ImGui::InputText("Class", buffer, sizeof(buffer))) + if (ImGui::InputText("Class", buffer, sizeof(buffer))) + { + component.ClassName = buffer; + return; + } + } + else { - component.ClassName = buffer; - return; + if (ImGui::InputText("Class", buffer, sizeof(buffer))) + { + component.ClassName = buffer; + return; + } } // Fields diff --git a/Resources/Branding/LuxEngineLogo.png b/Resources/Branding/LuxEngineLogo.png index 9092f58..e20b2d9 100644 Binary files a/Resources/Branding/LuxEngineLogo.png and b/Resources/Branding/LuxEngineLogo.png differ