diff --git a/source/editor/Editor.cpp b/source/editor/Editor.cpp index ee43d7b9e..8f07da8a5 100644 --- a/source/editor/Editor.cpp +++ b/source/editor/Editor.cpp @@ -31,6 +31,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "ImGui/Implementation/imgui_impl_sdl3.h" #include "Widgets/AssetBrowser.h" #include "Widgets/Console.h" +#include "Widgets/LightAuditor.h" #include "Widgets/Style.h" #include "Widgets/ProgressDialog.h" #include "Widgets/Properties.h" @@ -95,6 +96,7 @@ Editor::Editor(const vector& args) m_widgets.emplace_back(make_shared(this)); m_widgets.emplace_back(make_shared(this)); m_widgets.emplace_back(make_shared(this)); + m_widgets.emplace_back(make_shared(this)); m_widgets.emplace_back(make_shared(this)); m_widgets.emplace_back(make_shared(this)); m_widgets.emplace_back(make_shared(this)); diff --git a/source/editor/Widgets/LightAuditor.cpp b/source/editor/Widgets/LightAuditor.cpp new file mode 100644 index 000000000..5219032b2 --- /dev/null +++ b/source/editor/Widgets/LightAuditor.cpp @@ -0,0 +1,331 @@ +/* +Copyright(c) 2015-2026 Panos Karabelas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +//= INCLUDES ======================== +#include "pch.h" +#include "LightAuditor.h" + +#include "ButtonColorPicker.h" +#include "../ImGui/ImGui_Extension.h" +#include "World/Entity.h" +#include "World/Components/Light.h" +//=================================== + +//= NAMESPACES =============== +using namespace std; +using namespace spartan; +using namespace spartan::math; +//============================ + +namespace +{ + int resource_search_count = 0; + vector light_types = { "Directional", "Point", "Spot", "Area" }; + vector intensity_types = { + "Stadium", + "500W Bulb", + "150W Bulb", + "100W Bulb", + "60W Bulb", + "25W Bulb", + "Flashlight", + "Black Hole", + "Custom" + }; + + bool contains_search_ignore_case(const char* cstr_haystack, const char* cstr_needle) + { + string_view str_h = cstr_haystack; + string_view str_n = cstr_needle; + + const auto it = ranges::search(str_h, str_n, + [](unsigned char a, unsigned char b) + { + return std::tolower(a) == std::tolower(b); + }).begin(); + + return it != str_h.end(); + } + + bool is_resource_searched(const Light* light, const char* cstr_needle) + { + return contains_search_ignore_case(light->GetEntity()->GetObjectName().c_str(), cstr_needle); + } +} + +LightAuditor::LightAuditor(Editor* editor) : Widget(editor) +{ + m_title = "Light Auditor"; + m_visible = false; + m_alpha = 1.0f; + m_size_initial = Vector2(Display::GetWidth() * 0.25f, Display::GetHeight() * 0.5f); +} + +void LightAuditor::OnTickVisible() +{ + auto lights = World::GetEntitiesLights(); + + static char search_buffer[128] = ""; + ImGui::InputTextWithHint("##light_auditor_search", "Search light entity by name", search_buffer, IM_ARRAYSIZE(search_buffer)); + if (search_buffer[0] != '\0') + { + ImGui::SameLine(); + ImGui::Text("%d result%s", resource_search_count, resource_search_count > 1 ? "s" : ""); + resource_search_count = 0; + } + ImGui::Separator(); + + static ImGuiTableFlags flags = + ImGuiTableFlags_Borders | // Draw all borders. + ImGuiTableFlags_RowBg | // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_SizingFixedFit | // Match column width with its content's maximum width. + ImGuiTableFlags_Reorderable | // Allow reordering columns. + ImGuiTableFlags_Sortable | // Allow sorting rows. + ImGuiTableFlags_ContextMenuInBody | // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + ImGuiTableFlags_ScrollX | // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this create a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY; // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + + static ImVec2 size = ImGui::GetContentRegionAvail(); + if (ImGui::BeginTable("##Widget_LightAuditor", 16, flags, size)) + { + // Headers + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Type"); + ImGui::TableSetupColumn("Active"); + ImGui::TableSetupColumn("Color"); + ImGui::TableSetupColumn("Temperature"); + ImGui::TableSetupColumn("Intensity"); + ImGui::TableSetupColumn("Preset"); + ImGui::TableSetupColumn("Shadows"); + ImGui::TableSetupColumn("Screen Space"); + ImGui::TableSetupColumn("Volumetric"); + ImGui::TableSetupColumn("Cycle"); + ImGui::TableSetupColumn("Real Time"); + ImGui::TableSetupColumn("Range"); + ImGui::TableSetupColumn("Angle"); + ImGui::TableSetupColumn("Width"); + ImGui::TableSetupColumn("Height"); + ImGui::TableHeadersRow(); + + // --- Sorting logic on column header click --- + static int sorted_column = 1; // default sorting method by ID + static ImGuiSortDirection sort_direction = ImGuiSortDirection_Ascending; + + if (ImGuiTableSortSpecs* table_sort_specs = ImGui::TableGetSortSpecs()) + { + if (table_sort_specs->SpecsDirty) + { + const ImGuiTableColumnSortSpecs* spec = &table_sort_specs->Specs[0]; + sorted_column = spec->ColumnIndex; + sort_direction = spec->SortDirection; + table_sort_specs->SpecsDirty = false; + } + } + + ranges::sort(lights, [](Entity* a, Entity* b) + { + Light* light_a = a->GetComponent(); + Light* light_b = b->GetComponent(); + if (!light_a || !light_b) return false; + + switch (sorted_column) + { + case 0: return sort_direction == ImGuiSortDirection_Ascending // Name + ? a->GetObjectName() < b->GetObjectName() + : a->GetObjectName() > b->GetObjectName(); + case 1: return sort_direction == ImGuiSortDirection_Ascending // Type + ? light_a->GetLightType() < light_b->GetLightType() + : light_a->GetLightType() > light_b->GetLightType(); + case 2: return sort_direction == ImGuiSortDirection_Ascending // Active + ? a->GetActive() < b->GetActive() + : a->GetActive() > b->GetActive(); + case 3: return sort_direction == ImGuiSortDirection_Ascending // Color (sort based on luminance values) + ? light_a->GetColor().GetLuminance() < light_b->GetColor().GetLuminance() + : light_a->GetColor().GetLuminance() > light_b->GetColor().GetLuminance(); + case 4: return sort_direction == ImGuiSortDirection_Ascending // Temperature + ? light_a->GetTemperature() < light_b->GetTemperature() + : light_a->GetTemperature() > light_b->GetTemperature(); + case 5: return sort_direction == ImGuiSortDirection_Ascending // Intensity + ? light_a->GetIntensity() < light_b->GetIntensity() + : light_a->GetIntensity() > light_b->GetIntensity(); + case 6: return sort_direction == ImGuiSortDirection_Ascending // Preset + ? light_a->GetPreset() < light_b->GetPreset() + : light_a->GetPreset() > light_b->GetPreset(); + case 7: return sort_direction == ImGuiSortDirection_Ascending // Shadows Enabled + ? light_a->GetFlag(spartan::LightFlags::Shadows) < light_b->GetFlag(spartan::LightFlags::Shadows) + : light_a->GetFlag(spartan::LightFlags::Shadows) > light_b->GetFlag(spartan::LightFlags::Shadows); + case 8: return sort_direction == ImGuiSortDirection_Ascending // Shadows Screen Space + ? light_a->GetFlag(spartan::LightFlags::ShadowsScreenSpace) < light_b->GetFlag(spartan::LightFlags::ShadowsScreenSpace) + : light_a->GetFlag(spartan::LightFlags::ShadowsScreenSpace) > light_b->GetFlag(spartan::LightFlags::ShadowsScreenSpace); + case 9: return sort_direction == ImGuiSortDirection_Ascending // Volumetric + ? light_a->GetFlag(spartan::LightFlags::Volumetric) < light_b->GetFlag(spartan::LightFlags::Volumetric) + : light_a->GetFlag(spartan::LightFlags::Volumetric) > light_b->GetFlag(spartan::LightFlags::Volumetric); + case 10: return sort_direction == ImGuiSortDirection_Ascending // Day/Night Cycle + ? light_a->GetFlag(spartan::LightFlags::DayNightCycle) < light_b->GetFlag(spartan::LightFlags::DayNightCycle) + : light_a->GetFlag(spartan::LightFlags::DayNightCycle) > light_b->GetFlag(spartan::LightFlags::DayNightCycle); + case 11: return sort_direction == ImGuiSortDirection_Ascending // Real-Time Cycle + ? light_a->GetFlag(spartan::LightFlags::RealTimeCycle) < light_b->GetFlag(spartan::LightFlags::RealTimeCycle) + : light_a->GetFlag(spartan::LightFlags::RealTimeCycle) > light_b->GetFlag(spartan::LightFlags::RealTimeCycle); + case 12: return sort_direction == ImGuiSortDirection_Ascending // Range + ? light_a->GetRange() < light_b->GetRange() + : light_a->GetRange() > light_b->GetRange(); + case 13: return sort_direction == ImGuiSortDirection_Ascending // Angle + ? light_a->GetAngle() < light_b->GetAngle() + : light_a->GetAngle() > light_b->GetAngle(); + case 14: return sort_direction == ImGuiSortDirection_Ascending // Width + ? light_a->GetAreaWidth() < light_b->GetAreaWidth() + : light_a->GetAreaWidth() > light_b->GetAreaWidth(); + case 15: return sort_direction == ImGuiSortDirection_Ascending // Height + ? light_a->GetAreaWidth() < light_b->GetAreaWidth() + : light_a->GetAreaWidth() > light_b->GetAreaWidth(); + default: return true; + } + }); + + // --- Draw Row Data --- + for (auto& light_entity : lights) + { + if (Light* light = light_entity->GetComponent()) + { + if (search_buffer[0] != '\0') + { + if (!is_resource_searched(light, search_buffer)) + continue; + + resource_search_count++; + } + + const bool is_directional = light->GetLightType() == LightType::Directional; + const bool is_spot = light->GetLightType() == LightType::Spot; + const bool is_area = light->GetLightType() == LightType::Area; + + // Switch row + ImGui::TableNextRow(); + + // Name + ImGui::TableSetColumnIndex(0); + ImGui::Text(light_entity->GetObjectName().c_str()); + + // Type + ImGui::TableSetColumnIndex(1); + const uint32_t light_type_index = static_cast(light->GetLightType()); + ImGui::Text(light_types[light_type_index].c_str()); + + auto light_id_str = string(to_string(light_entity->GetObjectId())); + + // Active + ImGui::TableSetColumnIndex(2); + bool is_active = light_entity->IsActive(); + ImGui::BeginDisabled(); + ImGuiSp::toggle_switch(("##Active_" + light_id_str).c_str(), &is_active); + ImGui::EndDisabled(); + + // Color + ImGui::TableSetColumnIndex(3); + ImGui::BeginDisabled(); + const Color light_color = light->GetColor(); + ImVec4 light_color_im = ImVec4(light_color.r, light_color.g, light_color.b, light_color.a); + ImGui::ColorButton(("##Color_" + light_id_str).c_str(), light_color_im); + ImGui::EndDisabled(); + + // Temperature + ImGui::TableSetColumnIndex(4); + ImGui::Text("%.0f K", light->GetTemperature()); + + // Intensity + ImGui::TableSetColumnIndex(5); + ImGui::Text(is_directional ? "%.0f lux" : "%.0f lm", light->GetIntensityLumens()); + + // Presets + ImGui::TableSetColumnIndex(6); + const uint32_t intensity_type_index = static_cast(light->GetIntensity()); + ImGui::Text(!is_directional ? intensity_types[intensity_type_index].c_str() : ""); + + // Shadows Enabled + ImGui::TableSetColumnIndex(7); + bool shadows_enabled = light->GetFlag(spartan::LightFlags::Shadows); + ImGui::BeginDisabled(); + ImGuiSp::toggle_switch(("##Shadows_" + light_id_str).c_str(), &shadows_enabled); + ImGui::EndDisabled(); + + // Screen Space + ImGui::TableSetColumnIndex(8); + bool screen_space = light->GetFlag(spartan::LightFlags::ShadowsScreenSpace); + ImGui::BeginDisabled(); + ImGuiSp::toggle_switch(("##ScreenSpace_" + light_id_str).c_str(), &screen_space); + ImGui::EndDisabled(); + + // Volumetric + ImGui::TableSetColumnIndex(9); + bool volumetric = light->GetFlag(spartan::LightFlags::Volumetric); + ImGui::BeginDisabled(); + ImGuiSp::toggle_switch(("##Volumetric_" + light_id_str).c_str(), &volumetric); + ImGui::EndDisabled(); + + // Day/Night Cycle + ImGui::TableSetColumnIndex(10); + if (is_directional) + { + bool time_cycle = light->GetFlag(spartan::LightFlags::DayNightCycle); + ImGui::BeginDisabled(); + ImGuiSp::toggle_switch(("##DayNightCycle_" + light_id_str).c_str(), &time_cycle); + ImGui::EndDisabled(); + } + else + { + ImGui::Text(""); + } + + // Real-Time + ImGui::TableSetColumnIndex(11); + if (is_directional) + { + bool real_time = light->GetFlag(spartan::LightFlags::RealTimeCycle); + ImGui::BeginDisabled(); + ImGuiSp::toggle_switch(("##RealTime_" + light_id_str).c_str(), &real_time); + ImGui::EndDisabled(); + } + else + { + ImGui::Text(""); + } + + // Range + ImGui::TableSetColumnIndex(12); + ImGui::Text(!is_directional ? "%.1f m" : "", light->GetRange()); + + // Angle + ImGui::TableSetColumnIndex(13); + ImGui::Text(is_spot ? "%.1f d" : "", light->GetRange()); + + // Width + ImGui::TableSetColumnIndex(14); + ImGui::Text(is_area ? "%.2f m" : "", light->GetAreaWidth()); + + // Height + ImGui::TableSetColumnIndex(15); + ImGui::Text(is_area ? "%.2f m" : "", light->GetAreaHeight()); + } + } + + ImGui::EndTable(); + } +} diff --git a/source/editor/Widgets/LightAuditor.h b/source/editor/Widgets/LightAuditor.h new file mode 100644 index 000000000..3f9ee68ac --- /dev/null +++ b/source/editor/Widgets/LightAuditor.h @@ -0,0 +1,34 @@ +/* +Copyright(c) 2015-2026 Panos Karabelas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#pragma once + +//= INCLUDES ====== +#include "Widget.h" +//================= + +class LightAuditor : public Widget +{ +public: + LightAuditor(Editor* editor); + + void OnTickVisible() override; +}; diff --git a/source/editor/Widgets/MenuBar.cpp b/source/editor/Widgets/MenuBar.cpp index 643e4a1b0..32d349715 100644 --- a/source/editor/Widgets/MenuBar.cpp +++ b/source/editor/Widgets/MenuBar.cpp @@ -37,6 +37,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "Engine.h" #include "Profiling/RenderDoc.h" #include "Debugging.h" +#include "LightAuditor.h" #include "ScriptEditor.h" #include "Core/Definitions.h" #include "Core/ThreadPool.h" @@ -221,6 +222,7 @@ namespace menu_entry(); menu_entry(); menu_entry(); + menu_entry(); menu_entry(); menu_entry(); menu_entry(); @@ -501,6 +503,7 @@ void MenuBar::Initialize(Editor* _editor) file_dialog = make_unique(true, FileDialog_Type_FileSelection, FileDialog_Op_Open, FileDialog_Filter_World); buttons_toolbar::widgets[spartan::ResourceCache::GetIcon(spartan::IconType::Profiler)] = editor->GetWidget(); + buttons_toolbar::widgets[spartan::ResourceCache::GetIcon(spartan::IconType::Light)] = editor->GetWidget(); buttons_toolbar::widgets[spartan::ResourceCache::GetIcon(spartan::IconType::ResourceCache)] = editor->GetWidget(); buttons_toolbar::widgets[spartan::ResourceCache::GetIcon(spartan::IconType::Shader)] = editor->GetWidget(); buttons_toolbar::widgets[spartan::ResourceCache::GetIcon(spartan::IconType::Gear)] = editor->GetWidget(); diff --git a/source/runtime/Rendering/Color.cpp b/source/runtime/Rendering/Color.cpp index 202504ef0..13d5986f7 100644 --- a/source/runtime/Rendering/Color.cpp +++ b/source/runtime/Rendering/Color.cpp @@ -96,6 +96,11 @@ namespace spartan this->a = a; } + float Color::GetLuminance() const + { + return 0.21216f * r + 0.7152f * g + 0.04152f * b; + } + // standard const Color Color::standard_black = Color(0.0f, 0.0f, 0.0f, 1.0f); const Color Color::standard_white = Color(1.0f, 1.0f, 1.0f, 1.0f); diff --git a/source/runtime/Rendering/Color.h b/source/runtime/Rendering/Color.h index 11c475ce2..4445b7837 100644 --- a/source/runtime/Rendering/Color.h +++ b/source/runtime/Rendering/Color.h @@ -47,6 +47,8 @@ namespace spartan float b = 0.0f; float a = 0.0f; + float GetLuminance() const; + const float* Data() const { return &r; } // standard