diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d261f3f..1ad43e98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ set(VERSION_STRING "${VERSION_YEAR}.${VERSION_MONTH}.${VERSION_DAY}") set(CMAKE_CXX_STANDARD 20) file(GLOB HEADERS "*.h" +"DevConsole/*.h" "Console/*.h" "McZapkie/*.h" "gl/*.h" @@ -200,6 +201,8 @@ set(SOURCES "network/manager.cpp" "network/backend/asio.cpp" +"DevConsole/devconsole.cpp" + "widgets/vehiclelist.cpp" "widgets/map.cpp" "widgets/map_objects.cpp" @@ -410,7 +413,25 @@ configure_file(${CMAKE_SOURCE_DIR}/eu07.rc.in if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) set(SOURCES ${SOURCES} ${CMAKE_BINARY_DIR}/eu07.rc) - set(SOURCES ${SOURCES} eu07.ico) + set(SOURCES ${SOURCES} eu07.ico + entitysystem/systems/ParticlesSystem.cpp + entitysystem/systems/ParticlesSystem.h + entitysystem/systems/MeshRenderSystem.cpp + entitysystem/systems/MeshRenderSystem.h + entitysystem/systems/LODSystem.cpp + entitysystem/systems/LODSystem.h + entitysystem/systems/SoundSystem.cpp + entitysystem/systems/SoundSystem.h + entitysystem/systems/LightSystem.cpp + entitysystem/systems/LightSystem.h + entitysystem/systems/HierarchySystem.cpp + entitysystem/systems/HierarchySystem.h + entitysystem/systems/LineSystem.cpp + entitysystem/systems/LineSystem.h + entitysystem/systems/VehicleSyncSystem.cpp + entitysystem/systems/VehicleSyncSystem.h + entitysystem/systems/BillboardSystem.cpp + entitysystem/systems/BillboardSystem.h) add_compile_options("/utf-8") endif() diff --git a/DevConsole/devconsole.cpp b/DevConsole/devconsole.cpp new file mode 100644 index 00000000..bdc25b48 --- /dev/null +++ b/DevConsole/devconsole.cpp @@ -0,0 +1,379 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "DevConsole/devconsole.h" + +#include +#include +#include + +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +namespace dev { + +// ----------------------------------------------------------------------- +// Global singleton +// ----------------------------------------------------------------------- +console_panel Console; + +// ----------------------------------------------------------------------- +// helpers +// ----------------------------------------------------------------------- +static std::string str_tolower(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return std::tolower(c); }); + return s; +} + +// Tokenise a command line. Handles "quoted tokens" as a single arg. +static args_t tokenise(const std::string &line) +{ + args_t tokens; + std::istringstream ss(line); + std::string tok; + while (ss >> std::quoted(tok)) + tokens.push_back(tok); + return tokens; +} + +// ----------------------------------------------------------------------- +// console_panel +// ----------------------------------------------------------------------- +console_panel::console_panel() + : ui_panel("DevConsole", false) +{ + title = "Developer Console"; + + register_builtins(); +} + +void console_panel::register_builtins() +{ + register_command("help", "List all commands, or describe one: help [command]", + [this](const args_t &args) { + if (args.size() >= 2) { + auto it = m_commands.find(str_tolower(args[1])); + if (it == m_commands.end()) { + print_error("Unknown command: " + args[1]); + } else { + print_ok(it->second.name + " — " + it->second.description); + } + return; + } + print_info("Available commands:"); + std::vector names; + names.reserve(m_commands.size()); + for (auto &kv : m_commands) + names.push_back(kv.first); + std::sort(names.begin(), names.end()); + for (auto &n : names) { + std::string line = " " + n; + auto desc = m_commands[n].description; + if (!desc.empty()) + line += " — " + desc; + print(line); + } + }); + + register_command("clear", "Clear the console output", + [this](const args_t &) { + m_log.clear(); + }); + + register_command("ping", "Responds with pong", + [this](const args_t &) { + print_ok("pong"); + }); + + register_command("echo", "Print arguments back: echo [text...]", + [this](const args_t &args) { + std::string msg; + for (std::size_t i = 1; i < args.size(); ++i) { + if (i > 1) msg += ' '; + msg += args[i]; + } + print(msg); + }); + + register_command("history", "Show command history", + [this](const args_t &) { + if (m_history.empty()) { + print_info("(history is empty)"); + return; + } + int idx = 0; + for (auto &h : m_history) + print(" " + std::to_string(idx++) + ": " + h); + }); +} + +// ----------------------------------------------------------------------- +// Public API +// ----------------------------------------------------------------------- +void console_panel::register_command(std::string name, std::string description, + command_handler_t handler) +{ + std::string key = str_tolower(name); + m_commands[key] = console_command{ std::move(name), std::move(description), + std::move(handler) }; +} + +void console_panel::print(const std::string &text, glm::vec4 color) +{ + // Split multi-line strings into separate entries + std::istringstream ss(text); + std::string line; + while (std::getline(ss, line)) + m_log.push_back({ line, color }); + + while (m_log.size() > MAX_LOG) + m_log.pop_front(); + + m_scroll_to_bottom = true; +} + +void console_panel::print_info(const std::string &text) +{ + print("[info] " + text, { 0.6f, 0.8f, 1.0f, 1.0f }); +} + +void console_panel::print_ok(const std::string &text) +{ + print("[ok] " + text, { 0.42f, 0.80f, 0.23f, 1.0f }); +} + +void console_panel::print_error(const std::string &text) +{ + print("[error] " + text, { 1.0f, 0.35f, 0.35f, 1.0f }); +} + +void console_panel::print_warn(const std::string &text) +{ + print("[warn] " + text, { 1.0f, 0.80f, 0.20f, 1.0f }); +} + +void console_panel::execute(const std::string &input) +{ + std::string trimmed = input; + // ltrim / rtrim + trimmed.erase(0, trimmed.find_first_not_of(" \t\r\n")); + trimmed.erase(trimmed.find_last_not_of(" \t\r\n") + 1); + + if (trimmed.empty()) + return; + + // Echo the input + print("> " + trimmed, { 0.42f, 0.64f, 0.23f, 1.0f }); + + // Save to history (no duplicates at the end) + if (m_history.empty() || m_history.back() != trimmed) + m_history.push_back(trimmed); + if (m_history.size() > MAX_HIST) + m_history.pop_front(); + m_history_pos = -1; + + auto tokens = tokenise(trimmed); + if (tokens.empty()) + return; + + std::string cmd_name = str_tolower(tokens[0]); + execute_command(cmd_name, tokens); +} + +void console_panel::execute_command(const std::string &name, const args_t &args) +{ + auto it = m_commands.find(name); + if (it == m_commands.end()) { + print_error("Unknown command '" + name + "'. Type 'help' for a list."); + return; + } + try { + it->second.handler(args); + } + catch (const std::exception &e) { + print_error(std::string("Exception: ") + e.what()); + } +} + +std::vector console_panel::completions_for(const std::string &prefix) const +{ + std::string lp = str_tolower(prefix); + std::vector result; + for (auto &kv : m_commands) + if (kv.first.substr(0, lp.size()) == lp) + result.push_back(kv.first); + std::sort(result.begin(), result.end()); + return result; +} + +// ----------------------------------------------------------------------- +// ImGui InputText callback +// ----------------------------------------------------------------------- +struct CallbackUserData { + console_panel *self; + // current text in the buffer at callback time + std::string completion_prefix; +}; + +int console_panel::input_callback(ImGuiInputTextCallbackData *data) +{ + auto *ud = static_cast(data->UserData); + console_panel *self = ud->self; + + if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) { + // Collect the word being typed (last space-delimited token) + const char *end = data->Buf + data->CursorPos; + const char *begin = end; + while (begin > data->Buf && *(begin - 1) != ' ') + --begin; + + std::string prefix(begin, end); + auto candidates = self->completions_for(prefix); + + if (candidates.empty()) { + // nothing + } else if (candidates.size() == 1) { + // Replace the partial token with the full match + space + data->DeleteChars(static_cast(begin - data->Buf), + static_cast(end - begin)); + data->InsertChars(data->CursorPos, candidates[0].c_str()); + data->InsertChars(data->CursorPos, " "); + } else { + // Find common prefix among candidates + std::string common = candidates[0]; + for (std::size_t i = 1; i < candidates.size(); ++i) { + std::size_t j = 0; + while (j < common.size() && j < candidates[i].size() && + common[j] == candidates[i][j]) + ++j; + common.resize(j); + } + if (common.size() > prefix.size()) { + data->DeleteChars(static_cast(begin - data->Buf), + static_cast(end - begin)); + data->InsertChars(data->CursorPos, common.c_str()); + } + // Show all candidates + self->print_info("Candidates:"); + for (auto &c : candidates) + self->print(" " + c); + } + } + else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { + const int prev_pos = self->m_history_pos; + + if (data->EventKey == ImGuiKey_UpArrow) { + if (self->m_history_pos == -1) + self->m_history_pos = static_cast(self->m_history.size()) - 1; + else if (self->m_history_pos > 0) + --self->m_history_pos; + } else if (data->EventKey == ImGuiKey_DownArrow) { + if (self->m_history_pos != -1) { + ++self->m_history_pos; + if (self->m_history_pos >= static_cast(self->m_history.size())) + self->m_history_pos = -1; + } + } + + if (prev_pos != self->m_history_pos) { + const char *entry = (self->m_history_pos >= 0) + ? self->m_history[self->m_history_pos].c_str() + : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, entry); + } + } + + return 0; +} + +// ----------------------------------------------------------------------- +// Rendering +// ----------------------------------------------------------------------- +void console_panel::render() +{ + if (!is_open) + return; + + ImGui::SetNextWindowSize(ImVec2(700, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSizeConstraints(ImVec2(400, 200), ImVec2(FLT_MAX, FLT_MAX)); + + auto const panelname = (title.empty() ? m_name : title) + "###" + m_name; + if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoCollapse)) + render_contents(); + ImGui::End(); +} + +void console_panel::render_contents() +{ + // --- Log area --- + const float footer_height = ImGui::GetStyle().ItemSpacing.y + + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("console_scrolling", + ImVec2(0, -footer_height), + false, + ImGuiWindowFlags_HorizontalScrollbar); + + ImGui::PushFont(ui_layer::font_mono); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); + + for (auto &entry : m_log) { + ImGui::PushStyleColor(ImGuiCol_Text, + ImVec4(entry.color.r, entry.color.g, + entry.color.b, entry.color.a)); + ImGui::TextUnformatted(entry.text.c_str()); + ImGui::PopStyleColor(); + } + + if (m_scroll_to_bottom || + ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + m_scroll_to_bottom = false; + + ImGui::PopStyleVar(); + ImGui::PopFont(); + ImGui::EndChild(); + + ImGui::Separator(); + + // --- Input line --- + ImGui::PushFont(ui_layer::font_mono); + + bool reclaim_focus = false; + + ImGuiInputTextFlags input_flags = + ImGuiInputTextFlags_EnterReturnsTrue | + ImGuiInputTextFlags_CallbackCompletion | + ImGuiInputTextFlags_CallbackHistory; + + CallbackUserData cbud{ this, "" }; + + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::InputText("##console_input", m_input_buf, sizeof(m_input_buf), + input_flags, &console_panel::input_callback, &cbud)) + { + execute(m_input_buf); + m_input_buf[0] = '\0'; + reclaim_focus = true; + } + + ImGui::PopFont(); + + // Auto-focus the input field when the window is shown + ImGui::SetItemDefaultFocus(); + if (reclaim_focus || m_want_focus) { + ImGui::SetKeyboardFocusHere(-1); + m_want_focus = false; + } +} + +} // namespace dev diff --git a/DevConsole/devconsole.h b/DevConsole/devconsole.h new file mode 100644 index 00000000..344bfa8b --- /dev/null +++ b/DevConsole/devconsole.h @@ -0,0 +1,100 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "application/uilayer.h" + +struct ImGuiInputTextCallbackData; + +namespace dev { + +// Parsed command arguments — name of the command is args[0] +using args_t = std::vector; + +// Handler signature: receives all tokens including the command name at [0] +using command_handler_t = std::function; + +struct console_command { + std::string name; + std::string description; + command_handler_t handler; +}; + +// ----------------------------------------------------------------------- +// console_panel +// Inherits from ui_panel. Embed as a member in any ui_layer subclass, +// then call add_external_panel() to register it. +// +// Toggle visibility with the backtick / grave-accent key (`) handled +// from ui_layer::on_key(). +// +// Register commands from anywhere via the static global instance: +// dev::Console.register_command("name", "desc", handler); +// ----------------------------------------------------------------------- +class console_panel : public ui_panel { +public: + console_panel(); + + void render() override; + void render_contents() override; + + // Print a line to the console output + void print(const std::string &text, glm::vec4 color = {0.9f, 0.9f, 0.9f, 1.0f}); + void print_info(const std::string &text); + void print_ok(const std::string &text); + void print_error(const std::string &text); + void print_warn(const std::string &text); + + // Register a new command. Duplicate names overwrite previous. + void register_command(std::string name, std::string description, command_handler_t handler); + + // Execute a raw input string (tokenises, looks up, calls handler) + void execute(const std::string &input); + + // Focus the input field next frame + void focus_input() { m_want_focus = true; } + +private: + struct log_entry { + std::string text; + glm::vec4 color; + }; + + void execute_command(const std::string &name, const args_t &args); + std::vector completions_for(const std::string &prefix) const; + + // ImGui InputText callback — used for history and tab-completion + static int input_callback(ImGuiInputTextCallbackData *data); + + std::unordered_map m_commands; + std::deque m_log; + std::deque m_history; // previously executed lines + int m_history_pos { -1 }; // -1 = new input + char m_input_buf[512] {}; + bool m_scroll_to_bottom { false }; + bool m_want_focus { false }; + + static constexpr std::size_t MAX_LOG = 1024; + static constexpr std::size_t MAX_HIST = 128; + + // Built-in commands registered in the constructor + void register_builtins(); +}; + +// Global singleton — accessible from anywhere after the header is included +extern console_panel Console; + +} // namespace dev diff --git a/application/driveruilayer.cpp b/application/driveruilayer.cpp index a89f19fd..9645e7a4 100644 --- a/application/driveruilayer.cpp +++ b/application/driveruilayer.cpp @@ -11,12 +11,17 @@ http://mozilla.org/MPL/2.0/. #include "application/driveruilayer.h" #include "utilities/Globals.h" +#include "utilities/utilities.h" #include "application/application.h" #include "utilities/translation.h" #include "simulation/simulation.h" #include "vehicle/Train.h" #include "model/AnimModel.h" #include "rendering/renderer.h" +#include "DevConsole/devconsole.h" +#include "entitysystem/ECScene.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" driver_ui::driver_ui() { @@ -36,6 +41,7 @@ driver_ui::driver_ui() add_external_panel(&m_mappanel); add_external_panel(&m_logpanel); add_external_panel(&m_perfgraphpanel); + add_external_panel(&dev::Console); add_external_panel(&m_cameraviewpanel); m_logpanel.is_open = false; @@ -65,6 +71,302 @@ driver_ui::driver_ui() m_trainingcardpanel.is_open = true; m_vehiclelist.is_open = true; } + + register_driver_commands(); +} + +void driver_ui::register_driver_commands() +{ + auto &con = dev::Console; + + // --- UI toggles --- + con.register_command("debug", "Toggle debug panel", + [this](const dev::args_t &) { + m_debugpanel.is_open = !m_debugpanel.is_open; + dev::Console.print_ok(m_debugpanel.is_open ? "Debug panel opened" : "Debug panel closed"); + }); + + con.register_command("map", "Toggle map panel", + [this](const dev::args_t &) { + m_mappanel.is_open = !m_mappanel.is_open; + }); + + con.register_command("aid", "Toggle driving-aid panel", + [this](const dev::args_t &) { + m_aidpanel.is_open = !m_aidpanel.is_open; + }); + + con.register_command("timetable", "Toggle timetable panel", + [this](const dev::args_t &) { + m_timetablepanel.is_open = !m_timetablepanel.is_open; + }); + + // --- Simulation control --- + con.register_command("pause", "Pause or unpause simulation", + [](const dev::args_t &) { + command_relay relay; + relay.post(user_command::pausetoggle, 0.0, 0.0, GLFW_PRESS, 0); + }); + + con.register_command("debugmode", "Toggle debug mode (enable/disable)", + [](const dev::args_t &args) { + command_relay relay; + relay.post(user_command::debugtoggle, 0.0, 0.0, GLFW_RELEASE, 0); + dev::Console.print_ok(std::string("Debug mode: ") + (DebugModeFlag ? "on" : "off")); + }); + + // --- Weather --- + con.register_command("overcast", "Set overcast level: overcast <0.0-1.0>", + [](const dev::args_t &args) { + if (args.size() < 2) { + dev::Console.print_info("Current overcast: " + std::to_string(Global.Overcast)); + return; + } + try { + float v = std::stof(args[1]); + v = std::max(0.0f, std::min(1.0f, v)); + Global.Overcast = v; + dev::Console.print_ok("Overcast set to " + std::to_string(v)); + } catch (...) { + dev::Console.print_error("Expected a float 0.0-1.0"); + } + }); + + // --- Screenshot --- + con.register_command("screenshot", "Take a screenshot", + [](const dev::args_t &) { + Application.queue_screenshot(); + dev::Console.print_ok("Screenshot queued"); + }); + + // --- Log --- + con.register_command("log", "Enable or disable file logging: log [on|off]", + [](const dev::args_t &args) { + if (args.size() < 2) { + dev::Console.print_info(std::string("File logging: ") + + ((Global.iWriteLogEnabled & 1) ? "on" : "off")); + return; + } + if (args[1] == "on") { + Global.iWriteLogEnabled |= 1; + dev::Console.print_ok("File logging enabled"); + } else if (args[1] == "off") { + Global.iWriteLogEnabled &= ~1; + dev::Console.print_ok("File logging disabled"); + } else { + dev::Console.print_error("Usage: log [on|off]"); + } + }); + + // --- ECS --- + con.register_command("ecs.list", "List all ECS entities (optional filter: ecs.list [name_fragment])", + [](const dev::args_t &args) { + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + std::string filter = args.size() >= 2 ? args[1] : ""; + int count = 0; + for (auto entity : world.GetEntities()) { + std::string name = std::to_string(static_cast(entity)); + if (auto *id = world.GetComponent(entity)) + name = id->Name.ToString(); + if (!filter.empty() && name.find(filter) == std::string::npos) + continue; + dev::Console.print(" [" + std::to_string(static_cast(entity)) + "] " + name); + ++count; + } + dev::Console.print_info("Total: " + std::to_string(count) + " entities"); + }); + + con.register_command("ecs.info", "Show components of entity by name: ecs.info ", + [](const dev::args_t &args) { + if (args.size() < 2) { dev::Console.print_error("Usage: ecs.info "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + + dev::Console.print_ok("Entity: " + args[1]); + if (auto *t = world.GetComponent(entity)) { + dev::Console.print(" Transform: pos=(" + + std::to_string(t->Position.x) + ", " + + std::to_string(t->Position.y) + ", " + + std::to_string(t->Position.z) + ")"); + } + if (auto *v = world.GetComponent(entity)) { + dev::Console.print(" Velocity: (" + + std::to_string(v->Value.x) + ", " + + std::to_string(v->Value.y) + ", " + + std::to_string(v->Value.z) + ")"); + } + if (world.HasComponent(entity)) + dev::Console.print(" MeshRenderer: yes"); + if (world.HasComponent(entity)) + dev::Console.print(" ParticleEmitter: yes"); + if (auto *id = world.GetComponent(entity)) + dev::Console.print(" ID: " + id->Name.ToString() + " #" + std::to_string(id->Id)); + }); + + con.register_command("ecs.setvel", "Set entity velocity: ecs.setvel ", + [](const dev::args_t &args) { + if (args.size() < 5) { dev::Console.print_error("Usage: ecs.setvel "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + + try { + glm::vec3 vel{ std::stof(args[2]), std::stof(args[3]), std::stof(args[4]) }; + if (auto *v = world.GetComponent(entity)) { + v->Value = vel; + dev::Console.print_ok("Velocity set"); + } else { + world.AddComponent(entity).Value = vel; + dev::Console.print_ok("Velocity component added and set"); + } + } catch (...) { dev::Console.print_error("Invalid numbers"); } + }); + + con.register_command("ecs.kill", "Destroy entity by name: ecs.kill ", + [](const dev::args_t &args) { + if (args.size() < 2) { dev::Console.print_error("Usage: ecs.kill "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + world.DestroyEntity(entity); + dev::Console.print_ok("Entity destroyed: " + args[1]); + }); + + con.register_command("ecs.count", "Show total entity count in current scene", + [](const dev::args_t &) { + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + dev::Console.print_info("Entities: " + std::to_string(scene->World().GetEntityCount())); + }); + + con.register_command("ecs.setpos", "Teleport entity to position: ecs.setpos ", + [](const dev::args_t &args) { + if (args.size() < 5) { dev::Console.print_error("Usage: ecs.setpos "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + + try { + glm::dvec3 pos{ std::stod(args[2]), std::stod(args[3]), std::stod(args[4]) }; + if (auto *t = world.GetComponent(entity)) { + t->Position = pos; + dev::Console.print_ok("Position set: (" + + args[2] + ", " + args[3] + ", " + args[4] + ")"); + } else { + dev::Console.print_error("Entity has no Transform component"); + } + } catch (...) { dev::Console.print_error("Invalid numbers"); } + }); + + con.register_command("ecs.spawn", "Spawn a named entity with Transform at position: ecs.spawn ", + [](const dev::args_t &args) { + if (args.size() < 5) { dev::Console.print_error("Usage: ecs.spawn "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + try { + glm::dvec3 pos{ std::stod(args[2]), std::stod(args[3]), std::stod(args[4]) }; + auto entity = world.CreateEntity(); + auto &id = world.AddComponent(entity); + id.Name = args[1]; + auto &t = world.AddComponent(entity); + t.Position = pos; + dev::Console.print_ok("Spawned entity '" + args[1] + "' at (" + + args[2] + ", " + args[3] + ", " + args[4] + ")"); + } catch (...) { dev::Console.print_error("Invalid position arguments"); } + }); + + con.register_command("ecs.find", "Find entities whose name contains a substring: ecs.find ", + [](const dev::args_t &args) { + if (args.size() < 2) { dev::Console.print_error("Usage: ecs.find "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + const std::string &fragment = args[1]; + int found = 0; + for (auto entity : world.GetEntities()) { + auto *id = world.GetComponent(entity); + if (!id) continue; + std::string name = id->Name.ToString(); + if (name.find(fragment) != std::string::npos) { + dev::Console.print(" [" + std::to_string(static_cast(entity)) + "] " + name); + ++found; + } + } + if (found == 0) + dev::Console.print_warn("No entities found matching: " + fragment); + else + dev::Console.print_info(std::to_string(found) + " match(es)"); + }); + + con.register_command("ecs.disable", "Disable entity (systems will skip it): ecs.disable ", + [](const dev::args_t &args) { + if (args.size() < 2) { dev::Console.print_error("Usage: ecs.disable "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + if (!world.HasComponent(entity)) + world.AddComponent(entity); + dev::Console.print_ok("Disabled: " + args[1]); + }); + + con.register_command("ecs.enable", "Re-enable a disabled entity: ecs.enable ", + [](const dev::args_t &args) { + if (args.size() < 2) { dev::Console.print_error("Usage: ecs.enable "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + world.RemoveComponent(entity); + dev::Console.print_ok("Enabled: " + args[1]); + }); + + con.register_command("ecs.addparticles", "Add a particle emitter to an entity: ecs.addparticles ", + [](const dev::args_t &args) { + if (args.size() < 2) { dev::Console.print_error("Usage: ecs.addparticles "); return; } + ECScene *scene = Application.sceneManager.CurrentScene(); + if (!scene) { dev::Console.print_warn("No active scene"); return; } + ECWorld &world = scene->World(); + + entt::entity entity = world.FindEntityByName(args[1]); + if (entity == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; } + + auto& emitter = world.AddComponent(entity); + emitter.isActive = true; + emitter.spawnRate = 20.0f; + emitter.particleLifetime = 2.5f; + emitter.gravity = glm::vec3(0.0f, 0.4f, 0.0f); + emitter.airResistance = 0.15f; + emitter.colorFade = glm::vec4(0.0f, 0.0f, 0.0f, -0.5f); + if (auto *t = world.GetComponent(entity)) + emitter.emitterLocation = glm::vec3(t->Position); + dev::Console.print_ok("ParticleEmitter added to: " + args[1]); + }); + + con.print_info("Driver console ready. Type 'help' for commands."); } void driver_ui::render_menu_contents() diff --git a/application/driveruilayer.h b/application/driveruilayer.h index 8ad1d775..27c2994e 100644 --- a/application/driveruilayer.h +++ b/application/driveruilayer.h @@ -53,6 +53,9 @@ private: // render() subclass details void render_() override; + // registers driver-specific console commands + void + register_driver_commands(); // members drivingaid_panel m_aidpanel { "Driving Aid", false }; diff --git a/application/uilayer.cpp b/application/uilayer.cpp index f979d284..e06fba84 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -24,6 +24,7 @@ http://mozilla.org/MPL/2.0/. #include "entitysystem/components/RenderComponents.h" #include "imgui/imgui_impl_glfw.h" +#include "DevConsole/devconsole.h" GLFWwindow *ui_layer::m_window{nullptr}; ImGuiIO *ui_layer::m_imguiio{nullptr}; @@ -105,6 +106,8 @@ ui_layer::ui_layer() if (Global.loading_log) add_external_panel(&m_logpanel); m_logpanel.size = {700, 400}; + + add_external_panel(&dev::Console); } ui_layer::~ui_layer() {} @@ -319,6 +322,15 @@ bool ui_layer::on_key(int const Key, int const Action) return true; } + // Backtick (`) toggles the developer console + if (Key == GLFW_KEY_GRAVE_ACCENT) + { + dev::Console.is_open = !dev::Console.is_open; + if (dev::Console.is_open) + dev::Console.focus_input(); + return true; + } + if (Key == GLFW_KEY_F9) { m_logpanel.is_open = !m_logpanel.is_open; @@ -744,6 +756,7 @@ void ui_layer::render_menu_contents() if (ImGui::BeginMenu(STR_C("Windows"))) { ImGui::MenuItem(STR_C("Log"), "F9", &m_logpanel.is_open); + ImGui::MenuItem(STR_C("Developer Console"), "`", &dev::Console.is_open); if (DebugModeFlag) { ImGui::MenuItem(STR_C("ImGui Demo"), nullptr, &m_imgui_demo); diff --git a/entitysystem/ECScene.cpp b/entitysystem/ECScene.cpp index 6e94ef79..f1e16f95 100644 --- a/entitysystem/ECScene.cpp +++ b/entitysystem/ECScene.cpp @@ -36,14 +36,21 @@ void ECScene::Unload() void ECScene::Update(float dt) { - if (!m_loaded){ - + if (!m_loaded) return; - } + m_systems.Update(m_world, dt); OnUpdate(dt); } +void ECScene::Render() +{ + if (!m_loaded) + return; + + m_systems.Render(m_world); +} + ECWorld& ECScene::World() { diff --git a/entitysystem/ECScene.h b/entitysystem/ECScene.h index a723a2d6..a5477145 100644 --- a/entitysystem/ECScene.h +++ b/entitysystem/ECScene.h @@ -21,6 +21,7 @@ public: void Load(); void Unload(); void Update(float dt); + void Render(); ECWorld& World(); const ECWorld& World() const; diff --git a/entitysystem/ECWorld.cpp b/entitysystem/ECWorld.cpp index 48ad91c8..2468d39c 100644 --- a/entitysystem/ECWorld.cpp +++ b/entitysystem/ECWorld.cpp @@ -52,3 +52,9 @@ const entt::registry& ECWorld::Registry() const { return m_registry; } + +ECWorld& GetComponentSystem() +{ + static ECWorld instance; + return instance; +} diff --git a/entitysystem/ECWorld.h b/entitysystem/ECWorld.h index f1a8ee11..f52ad044 100644 --- a/entitysystem/ECWorld.h +++ b/entitysystem/ECWorld.h @@ -66,6 +66,24 @@ public: }, components); } } + + // Same as Each but skips entities that have any of the Excluded components. + // Usage: world.Each(entt::exclude, lambda) + template + void Each(entt::exclude_t, Func&& func) + { + auto view = m_registry.view(entt::exclude); + + for (auto entity : view) + { + auto components = std::forward_as_tuple(view.get(entity)...); + + std::apply([&](Components&... comps) + { + func(entity, comps...); + }, components); + } + } std::vector GetEntities() const { @@ -96,11 +114,16 @@ T& ECWorld::AddComponent(entt::entity entity, Args&&... args) "Component must be constructible with provided arguments"); if (m_registry.all_of(entity)) - { - return m_registry.replace(entity, std::forward(args)...); - } + m_registry.replace(entity, std::forward(args)...); + else + m_registry.emplace(entity, std::forward(args)...); - return m_registry.emplace(entity, std::forward(args)...); + if constexpr (std::is_empty_v) { + static T dummy{}; + return dummy; + } else { + return m_registry.get(entity); + } } template diff --git a/entitysystem/SceneManager.cpp b/entitysystem/SceneManager.cpp index 564e7eec..3b79aefd 100644 --- a/entitysystem/SceneManager.cpp +++ b/entitysystem/SceneManager.cpp @@ -16,8 +16,6 @@ SceneManager::~SceneManager() void SceneManager::SetScene(std::unique_ptr scene) { m_pendingScene = std::move(scene); - if (m_currentScene) - m_currentScene->Load(); } void SceneManager::Update(float dt) diff --git a/entitysystem/components/BasicComponents.h b/entitysystem/components/BasicComponents.h index 60ead447..9b346d42 100644 --- a/entitysystem/components/BasicComponents.h +++ b/entitysystem/components/BasicComponents.h @@ -8,10 +8,12 @@ #include "glm/vec3.hpp" #include "glm/gtc/quaternion.hpp" #include "registry/FName.h" -#include "utils/uuid.hpp" +#include "utilities/uuid.hpp" #include #include +class TDynamicObject; + namespace ECSComponent { ///< summary> @@ -45,6 +47,15 @@ struct Identification struct Parent { entt::entity value{entt::null}; + glm::dvec3 localOffset{0.0}; // position offset relative to parent + glm::quat localRotation{1.f, 0.f, 0.f, 0.f}; // rotation relative to parent +}; + +// Links an ECS entity to its corresponding simulation vehicle. +// VehicleSyncSystem reads this to keep Transform in sync with vehicle world position. +struct VehicleRef +{ + TDynamicObject* vehicle = nullptr; }; struct SoundComponent diff --git a/entitysystem/components/RenderComponents.h b/entitysystem/components/RenderComponents.h index 8ae0473e..00d7be28 100644 --- a/entitysystem/components/RenderComponents.h +++ b/entitysystem/components/RenderComponents.h @@ -4,26 +4,23 @@ #include "registry/FName.h" #include "rendering/geometrybank.h" +class TAnimModel; + namespace ECSComponent { /// /// Component for entities that can be rendered. /// -/// Currently empty -/// TODO: Add component members -/// struct MeshRenderer { gfx::geometry_handle meshHandle; + TAnimModel* modelInstance = nullptr; bool visible = true; }; /// /// Component for entities that can cast shadows. /// -/// Currently empty -/// TODO: Add component members -/// struct SpotLight { glm::vec3 Color{ 1.0f, 1.0f, 1.0f }; @@ -38,16 +35,70 @@ struct SpotLight bool Enabled = true; }; -// TODO: AreaLight like blender -// TODO: SunLight for scene - - -struct ParticleEmitter +/// +/// +/// +struct AreaLight { - FName particleEffectPath = (""); - bool active = true; + glm::vec3 Color{ 1.0f, 1.0f, 1.0f }; + float Intensity = 1.0f; + float Width = 1.0f; + float Height = 1.0f; + bool CastShadows = false; + bool Enabled = true; }; +struct SunLight +{ + glm::vec3 Color{ 1.0f, 1.0f, 1.0f }; + float Intensity = 1.0f; + bool CastShadows = false; + bool Enabled = true; +}; + + +struct Particle { + glm::vec3 position; + glm::vec3 velocity; + glm::vec4 color; + float size; + float age; + float maxAge; +}; + +struct ParticleEmitter { + + // --- Kontener i Stan --- + std::vector particles; + uint32_t maxParticles = 1000; + bool isActive = true; + float spawnAccumulator = 0.0f; + + // --- Konfiguracja Emisji --- + float spawnRate = 20.0f; // cząstek na sekundę + float particleLifetime = 2.0f; // bazowy czas życia + glm::vec3 gravity{ 0.0f, 0.0f, 0.0f }; // np. dym: 0.5, deszcz: -9.81 + float airResistance = 0.1f; // tłumienie prędkości + + // --- Zakresy (losowanie) --- + glm::vec3 minStartVelocity{ -0.5f, 1.0f, -0.5f }; + glm::vec3 maxStartVelocity{ 0.5f, 2.0f, 0.5f }; + + // --- Ewolucja w czasie (Modyfikatory) --- + float sizeGrowth = 0.5f; // zmiana rozmiaru na sekundę + glm::vec4 colorFade = { 0.0f, 0.0f, 0.0f, -0.5f }; // np. znikanie (alpha) + + // --- Pozycja emitera --- + glm::vec3 emitterLocation{ 0.0f, 0.0f, 0.0f }; + + // --- Flag Funkcjonalnych --- + bool hasCollision = false; // czy sprawdzać kolizje + float bounceFactor = 0.3f; // energia po odbiciu (0.0 - 1.0) +}; + +/// +/// Component for entities that can be rendered as decals (e.g. graffiti on wagons, dirties on wall). +/// struct Decal { FName decalTexturePath = (""); @@ -55,6 +106,10 @@ struct Decal bool active = true; }; + +/// +/// Component for entities that can be rendered as billboards (e.g. particles, sprites). +/// struct Billboard { FName texturePath = (""); @@ -62,6 +117,9 @@ struct Billboard bool active = true; }; +/// +/// Component for entities that can be rendered as lines. +/// struct Line { glm::vec3 start{ 0.0f }; glm::vec3 end{ 1.0f, 0.0f, 0.0f }; diff --git a/entitysystem/scenes/GameScene.cpp b/entitysystem/scenes/GameScene.cpp index 711f124d..f4fab26e 100644 --- a/entitysystem/scenes/GameScene.cpp +++ b/entitysystem/scenes/GameScene.cpp @@ -4,32 +4,96 @@ #include "GameScene.h" - #include "entitysystem/components/BasicComponents.h" #include "entitysystem/components/RenderComponents.h" #include "entitysystem/systems/MovementSystem.h" +#include "entitysystem/systems/ParticlesSystem.h" +#include "entitysystem/systems/MeshRenderSystem.h" +#include "entitysystem/systems/LODSystem.h" +#include "entitysystem/systems/SoundSystem.h" +#include "entitysystem/systems/LightSystem.h" +#include "entitysystem/systems/HierarchySystem.h" +#include "entitysystem/systems/LineSystem.h" +#include "entitysystem/systems/VehicleSyncSystem.h" +#include "entitysystem/systems/BillboardSystem.h" #include "utilities/Logs.h" GameScene::GameScene() = default; -GameScene::~GameScene() = default ; +GameScene::~GameScene() = default; void GameScene::OnLoad() { + m_systems.AddSystem(); // must be first — updates Transform from live vehicle m_systems.AddSystem(); - + m_systems.AddSystem(); + m_systems.AddSystem(); + m_systems.AddSystem(); + m_systems.AddSystem(); + m_systems.AddSystem(); + m_systems.AddSystem(); + m_systems.AddSystem(); + m_systems.AddSystem(); + +#ifdef _DEBUG + CreateTestEntities(); +#endif } void GameScene::OnUpdate(float dt) { - - //auto& world = World(); - - //auto view = world.View(); - - -} +} void GameScene::OnUnload() { World().Clear(); +} + +void GameScene::CreateTestEntities() +{ + auto& world = World(); + + // smoke emitter at world origin for visual testing of ParticlesSystem + { + auto entity = world.CreateEntity(); + + auto& id = world.AddComponent(entity); + id.Name = "TestSmokeEmitter"; + + auto& transform = world.AddComponent(entity); + transform.Position = glm::dvec3(0.0, 2.0, 0.0); + + auto& emitter = world.AddComponent(entity); + emitter.emitterLocation = glm::vec3(0.0f, 2.0f, 0.0f); + emitter.isActive = true; + emitter.spawnRate = 15.0f; + emitter.particleLifetime = 3.0f; + emitter.gravity = glm::vec3(0.0f, 0.3f, 0.0f); + emitter.airResistance = 0.2f; + emitter.colorFade = glm::vec4(0.0f, 0.0f, 0.0f, -0.4f); + + WriteLog("[ECS] Created test entity: TestSmokeEmitter"); + } + + // moving entity to test MovementSystem + LOD culling + { + auto entity = world.CreateEntity(); + + auto& id = world.AddComponent(entity); + id.Name = "TestMovingBox"; + + auto& transform = world.AddComponent(entity); + transform.Position = glm::dvec3(10.0, 0.5, 0.0); + + auto& velocity = world.AddComponent(entity); + velocity.Value = glm::vec3(-1.0f, 0.0f, 0.0f); + + auto& lod = world.AddComponent(entity); + lod.RangeMin = 0.0; + lod.RangeMax = 200.0; + + auto& mesh = world.AddComponent(entity); + mesh.visible = true; + + WriteLog("[ECS] Created test entity: TestMovingBox"); + } } \ No newline at end of file diff --git a/entitysystem/systems/BaseSystem.h b/entitysystem/systems/BaseSystem.h index 4587af5b..3af90a6b 100644 --- a/entitysystem/systems/BaseSystem.h +++ b/entitysystem/systems/BaseSystem.h @@ -19,6 +19,7 @@ public: virtual void OnDestroy(ECWorld& world) {} virtual void Update(ECWorld& world, float dt) {} + virtual void Render(ECWorld& world) {} }; diff --git a/entitysystem/systems/BillboardSystem.cpp b/entitysystem/systems/BillboardSystem.cpp new file mode 100644 index 00000000..2773fb5a --- /dev/null +++ b/entitysystem/systems/BillboardSystem.cpp @@ -0,0 +1,116 @@ +#include "stdafx.h" +#include "BillboardSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +#include "gl/buffer.h" +#include "gl/vao.h" +#include "gl/shader.h" + +struct BillboardSystem::RenderState +{ + std::optional instanceBuffer; + std::optional vao; + std::unique_ptr shader; + std::size_t bufferCapacity { 0 }; +}; + +BillboardSystem::BillboardSystem() = default; +BillboardSystem::~BillboardSystem() = default; + +void BillboardSystem::Render(ECWorld& world) +{ + // Group billboard instances by texture path + std::unordered_map> groups; + + const glm::vec3 camPos{ + static_cast(Global.pCamera.Pos.x), + static_cast(Global.pCamera.Pos.y), + static_cast(Global.pCamera.Pos.z) + }; + + world.Each( + entt::exclude, + [&](entt::entity, ECSComponent::Billboard& bill, ECSComponent::Transform& t) + { + if (!bill.active) return; + std::string key = bill.texturePath.ToString(); + groups[key].push_back({ + glm::vec3(t.Position) - camPos, + bill.size + }); + }); + + if (groups.empty()) return; + + // Lazy-init shared GL resources + if (!m_renderState) + m_renderState = std::make_unique(); + + auto& rs = *m_renderState; + + if (!rs.instanceBuffer) + rs.instanceBuffer.emplace(); + + if (!rs.shader) { + try { + gl::shader vert("ecs_billboard.vert"); + gl::shader frag("ecs_billboard.frag"); + rs.shader = std::unique_ptr(new gl::program({vert, frag})); + } catch (const gl::shader_exception& e) { + WriteLog("[ECS] BillboardSystem: shader compile failed: " + std::string(e.what())); + return; + } + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + rs.shader->bind(); + + for (auto& [texPath, instances] : groups) { + // Grow VBO if needed + if (instances.size() > rs.bufferCapacity) { + rs.bufferCapacity = instances.size() + 64; + rs.instanceBuffer->allocate( + gl::buffer::ARRAY_BUFFER, + static_cast(rs.bufferCapacity * sizeof(BillboardInstance)), + GL_DYNAMIC_DRAW); + rs.vao.reset(); // rebuild after realloc + } + + rs.instanceBuffer->upload( + gl::buffer::ARRAY_BUFFER, + instances.data(), 0, + static_cast(instances.size() * sizeof(BillboardInstance))); + + if (!rs.vao) { + rs.vao.emplace(); + constexpr int stride = sizeof(BillboardInstance); + rs.vao->setup_attrib(*rs.instanceBuffer, 0, 3, GL_FLOAT, stride, 0); // position + glVertexAttribDivisor(0, 1); + rs.vao->setup_attrib(*rs.instanceBuffer, 1, 1, GL_FLOAT, stride, 12); // size + glVertexAttribDivisor(1, 1); + rs.vao->unbind(); + rs.instanceBuffer->unbind(gl::buffer::ARRAY_BUFFER); + } + + // Bind texture (fallback: skip group if path is empty) + if (!texPath.empty()) { + auto texHandle = GfxRenderer->Fetch_Texture(texPath, true); + GfxRenderer->Bind_Texture(0, texHandle); + } + + rs.vao->bind(); + glDrawArraysInstanced(GL_TRIANGLES, 0, 6, static_cast(instances.size())); + rs.vao->unbind(); + } + + gl::program::unbind(); + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); +} diff --git a/entitysystem/systems/BillboardSystem.h b/entitysystem/systems/BillboardSystem.h new file mode 100644 index 00000000..576c9844 --- /dev/null +++ b/entitysystem/systems/BillboardSystem.h @@ -0,0 +1,25 @@ +#pragma once + +#include "BaseSystem.h" +#include "rendering/renderer.h" +#include +#include + +// Renders Billboard components as camera-facing textured quads. +// Instances sharing the same texture are batched into one draw call. +class BillboardSystem : public BaseSystem +{ +public: + BillboardSystem(); + ~BillboardSystem(); + void Render(ECWorld& world) override; + +private: + struct BillboardInstance { + glm::vec3 position; // camera-relative + float size; + }; + + struct RenderState; + std::unique_ptr m_renderState; +}; diff --git a/entitysystem/systems/HierarchySystem.cpp b/entitysystem/systems/HierarchySystem.cpp new file mode 100644 index 00000000..1a005242 --- /dev/null +++ b/entitysystem/systems/HierarchySystem.cpp @@ -0,0 +1,27 @@ +#include "stdafx.h" +#include "HierarchySystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" + +void HierarchySystem::Update(ECWorld& world, float dt) +{ + world.Each( + entt::exclude, + [&](entt::entity, ECSComponent::Parent& parent, ECSComponent::Transform& childTransform) + { + if (parent.value == entt::null || !world.IsAlive(parent.value)) + return; + + const auto* parentTransform = world.GetComponent(parent.value); + if (!parentTransform) + return; + + // Rotate local offset by parent rotation then add parent world position. + glm::dvec3 rotatedOffset = glm::dvec3(parentTransform->Rotation * glm::vec3(parent.localOffset)); + childTransform.Position = parentTransform->Position + rotatedOffset; + + // Compose rotations: child world = parent world * child local. + childTransform.Rotation = parentTransform->Rotation * parent.localRotation; + }); +} diff --git a/entitysystem/systems/HierarchySystem.h b/entitysystem/systems/HierarchySystem.h new file mode 100644 index 00000000..43c8921a --- /dev/null +++ b/entitysystem/systems/HierarchySystem.h @@ -0,0 +1,14 @@ +#pragma once + +#include "BaseSystem.h" + +// Propagates parent world Transform to children. +// Runs after MovementSystem so children always inherit the parent's +// already-updated position in the same frame. +// Child's Transform.Position is stored as a LOCAL offset relative to +// the parent and this system overwrites it with the computed world position. +class HierarchySystem : public BaseSystem +{ +public: + void Update(ECWorld& world, float dt) override; +}; diff --git a/entitysystem/systems/LODSystem.cpp b/entitysystem/systems/LODSystem.cpp new file mode 100644 index 00000000..449ed17e --- /dev/null +++ b/entitysystem/systems/LODSystem.cpp @@ -0,0 +1,33 @@ +#include "stdafx.h" +#include "LODSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" +#include "utilities/Globals.h" +#include "model/AnimModel.h" + +void LODSystem::Update(ECWorld& world, float dt) +{ + const glm::dvec3 camPos{ + Global.pCamera.Pos.x, + Global.pCamera.Pos.y, + Global.pCamera.Pos.z + }; + + world.Each( + entt::exclude, + [&](entt::entity entity, + ECSComponent::LODController& lod, + ECSComponent::Transform& transform) + { + double dist = glm::length(camPos - transform.Position); + bool inRange = (dist >= lod.RangeMin) && (dist <= lod.RangeMax); + + if (auto* mesh = world.GetComponent(entity)) { + mesh->visible = inRange; + if (mesh->modelInstance) + mesh->modelInstance->visible(inRange); + } + }); +} diff --git a/entitysystem/systems/LODSystem.h b/entitysystem/systems/LODSystem.h new file mode 100644 index 00000000..de531117 --- /dev/null +++ b/entitysystem/systems/LODSystem.h @@ -0,0 +1,11 @@ +#pragma once + +#include "BaseSystem.h" + +// Reads LODController + Transform on each entity and toggles MeshRenderer.visible +// based on distance to camera. Runs every Update tick. +class LODSystem : public BaseSystem +{ +public: + void Update(ECWorld& world, float dt) override; +}; diff --git a/entitysystem/systems/LightSystem.cpp b/entitysystem/systems/LightSystem.cpp new file mode 100644 index 00000000..fd488109 --- /dev/null +++ b/entitysystem/systems/LightSystem.cpp @@ -0,0 +1,62 @@ +#include "stdafx.h" +#include "LightSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" +#include "utilities/Globals.h" +#include "rendering/lightarray.h" +#include "simulation/simulation.h" + +void LightSystem::Update(ECWorld& world, float dt) +{ + // Sync SunLight intensity with global overcast value. + // Overcast 0 = clear sky (full sun), 2 = heavy overcast (dim). + const float sunIntensity = glm::clamp( 1.0f - Global.Overcast * 0.4f, 0.1f, 1.0f ); + world.Each( + entt::exclude, + [sunIntensity](entt::entity, ECSComponent::SunLight& sun) + { + if (sun.Enabled) + sun.Intensity = sunIntensity; + }); + + // SpotLights: cull distant lights and rebuild the free_lights list for the renderer. + const glm::dvec3 camPos{ + Global.pCamera.Pos.x, + Global.pCamera.Pos.y, + Global.pCamera.Pos.z + }; + simulation::Lights.free_lights.clear(); + world.Each( + entt::exclude, + [&](entt::entity, ECSComponent::SpotLight& spot, ECSComponent::Transform& transform) + { + double dist = glm::length(camPos - transform.Position); + if (dist > static_cast(spot.Range) * 4.0) { + spot.Enabled = false; + return; + } + if (spot.Range > 0.0f) + spot.Enabled = true; + + if (!spot.Enabled) return; + + light_array::free_light_record rec; + rec.position = transform.Position; + rec.direction = glm::vec3(0.f, -1.f, 0.f); // default downward; rotation support can be added later + rec.color = spot.Color; + rec.intensity = spot.Intensity; + rec.range = spot.Range; + rec.inner_cutoff = glm::cos(glm::radians(spot.InnerAngle)); + rec.outer_cutoff = glm::cos(glm::radians(spot.OuterAngle)); + simulation::Lights.free_lights.push_back(rec); + }); +} + +void LightSystem::Render(ECWorld& world) +{ + // Rendering of ECS lights is handled by the main renderer pipeline, + // which queries SpotLight/AreaLight/SunLight components via World views. + // Nothing to do here for now. +} diff --git a/entitysystem/systems/LightSystem.h b/entitysystem/systems/LightSystem.h new file mode 100644 index 00000000..01cc3b5b --- /dev/null +++ b/entitysystem/systems/LightSystem.h @@ -0,0 +1,13 @@ +#pragma once + +#include "BaseSystem.h" + +// Manages ECS light components (SpotLight, AreaLight, SunLight). +// Updates light position/direction from Transform each tick. +// Actual renderer integration is handled by the rendering pipeline reading these components. +class LightSystem : public BaseSystem +{ +public: + void Update(ECWorld& world, float dt) override; + void Render(ECWorld& world) override; +}; diff --git a/entitysystem/systems/LineSystem.cpp b/entitysystem/systems/LineSystem.cpp new file mode 100644 index 00000000..e77b4d75 --- /dev/null +++ b/entitysystem/systems/LineSystem.cpp @@ -0,0 +1,96 @@ +#include "stdafx.h" +#include "LineSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +#include "gl/buffer.h" +#include "gl/vao.h" +#include "gl/shader.h" + +struct LineSystem::RenderState +{ + std::optional vertexBuffer; + std::optional vao; + std::unique_ptr shader; + std::size_t bufferCapacity { 0 }; +}; + +LineSystem::LineSystem() = default; +LineSystem::~LineSystem() = default; + +void LineSystem::Render(ECWorld& world) +{ + std::vector vertices; + vertices.reserve(256); + + const glm::vec3 camPos{ + static_cast(Global.pCamera.Pos.x), + static_cast(Global.pCamera.Pos.y), + static_cast(Global.pCamera.Pos.z) + }; + + world.Each( + entt::exclude, + [&](entt::entity, ECSComponent::Line& line, ECSComponent::Transform& t) + { + if (!line.active) return; + + glm::vec3 worldOrigin = glm::vec3(t.Position); + vertices.push_back({ worldOrigin + line.start - camPos, line.color }); + vertices.push_back({ worldOrigin + line.end - camPos, line.color }); + }); + + if (vertices.empty()) return; + + if (!m_renderState) + m_renderState = std::make_unique(); + + auto& rs = *m_renderState; + + if (!rs.vertexBuffer) + rs.vertexBuffer.emplace(); + + if (vertices.size() > rs.bufferCapacity) { + rs.bufferCapacity = vertices.size() + 64; + rs.vertexBuffer->allocate( + gl::buffer::ARRAY_BUFFER, + static_cast(rs.bufferCapacity * sizeof(LineVertex)), + GL_DYNAMIC_DRAW); + rs.vao.reset(); + } + + rs.vertexBuffer->upload( + gl::buffer::ARRAY_BUFFER, + vertices.data(), 0, + static_cast(vertices.size() * sizeof(LineVertex))); + + if (!rs.vao) { + rs.vao.emplace(); + constexpr int stride = sizeof(LineVertex); + rs.vao->setup_attrib(*rs.vertexBuffer, 0, 3, GL_FLOAT, stride, 0); // position + rs.vao->setup_attrib(*rs.vertexBuffer, 1, 3, GL_FLOAT, stride, 12); // color + rs.vao->unbind(); + rs.vertexBuffer->unbind(gl::buffer::ARRAY_BUFFER); + } + + if (!rs.shader) { + try { + gl::shader vert("vbocolor.vert"); + gl::shader frag("ecs_line.frag"); + rs.shader = std::unique_ptr(new gl::program({vert, frag})); + } catch (const gl::shader_exception& e) { + WriteLog("[ECS] LineSystem: shader compile failed: " + std::string(e.what())); + return; + } + } + + rs.shader->bind(); + rs.vao->bind(); + glDrawArrays(GL_LINES, 0, static_cast(vertices.size())); + rs.vao->unbind(); + gl::program::unbind(); +} diff --git a/entitysystem/systems/LineSystem.h b/entitysystem/systems/LineSystem.h new file mode 100644 index 00000000..56bc7b7b --- /dev/null +++ b/entitysystem/systems/LineSystem.h @@ -0,0 +1,22 @@ +#pragma once + +#include "BaseSystem.h" + +// Renders Line components as GL_LINES using camera-relative world positions. +// Uses vbocolor.vert + ecs_line.frag. +class LineSystem : public BaseSystem +{ +public: + LineSystem(); + ~LineSystem(); + void Render(ECWorld& world) override; + +private: + struct LineVertex { + glm::vec3 position; // camera-relative world position + glm::vec3 color; + }; + + struct RenderState; + std::unique_ptr m_renderState; +}; diff --git a/entitysystem/systems/MeshRenderSystem.cpp b/entitysystem/systems/MeshRenderSystem.cpp new file mode 100644 index 00000000..474a7353 --- /dev/null +++ b/entitysystem/systems/MeshRenderSystem.cpp @@ -0,0 +1,16 @@ +#include "stdafx.h" +#include "MeshRenderSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" + +// gfx_renderer does not expose per-handle draw calls — geometry is drawn +// by the legacy pipeline (simulation::Region → GfxRenderer->Render()). +// MeshRenderer.visible is the authoritative visibility flag; LODSystem writes +// it and the legacy scene node reads it when the renderer traverses the scene. +// When a custom ECS draw path is wired into the renderer this system will +// issue the actual draw calls. +void MeshRenderSystem::Render(ECWorld& world) +{ +} diff --git a/entitysystem/systems/MeshRenderSystem.h b/entitysystem/systems/MeshRenderSystem.h new file mode 100644 index 00000000..ddb59cfe --- /dev/null +++ b/entitysystem/systems/MeshRenderSystem.h @@ -0,0 +1,9 @@ +#pragma once + +#include "BaseSystem.h" + +class MeshRenderSystem : public BaseSystem +{ +public: + void Render(ECWorld& world) override; +}; diff --git a/entitysystem/systems/MovementSystem.cpp b/entitysystem/systems/MovementSystem.cpp index 6fe3bf61..ad9c856e 100644 --- a/entitysystem/systems/MovementSystem.cpp +++ b/entitysystem/systems/MovementSystem.cpp @@ -13,6 +13,7 @@ void MovementSystem::Update(ECWorld& world, float dt) constexpr double damping = 0.90; // 0.0 = instant stop, 1.0 = without damping world.Each( + entt::exclude, [dt](auto entity, auto& transform, auto& velocity) { @@ -22,7 +23,7 @@ void MovementSystem::Update(ECWorld& world, float dt) if (glm::length(velocity.Value) < 0.001) { - velocity.Value = glm::dvec3(0.0); + velocity.Value = glm::vec3(0.0f); } } ); diff --git a/entitysystem/systems/ParticlesSystem.cpp b/entitysystem/systems/ParticlesSystem.cpp new file mode 100644 index 00000000..1a3abc69 --- /dev/null +++ b/entitysystem/systems/ParticlesSystem.cpp @@ -0,0 +1,196 @@ +// +// Created by Daniu +// + +#include "stdafx.h" +#include "ParticlesSystem.h" + +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" +#include "utilities/Globals.h" + +#include "gl/buffer.h" +#include "gl/vao.h" +#include "gl/shader.h" + +#include "utilities/Logs.h" +#include +#include + +// ---- OpenGL resource bundle (pimpl) ---------------------------------------- + +struct ParticlesSystem::RenderState +{ + std::optional instanceBuffer; + std::optional vao; + std::unique_ptr shader; + std::size_t bufferCapacity { 0 }; +}; + +ParticlesSystem::ParticlesSystem() = default; +ParticlesSystem::~ParticlesSystem() = default; + +// ---- Update ---------------------------------------------------------------- + +void ParticlesSystem::Update(ECWorld &world, float dt) +{ + world.Each( + entt::exclude, + [&](entt::entity entity, ECSComponent::ParticleEmitter& emitter) + { + if (!emitter.isActive && emitter.particles.empty()) return; + + for (size_t i = 0; i < emitter.particles.size(); ) + { + auto& p = emitter.particles[i]; + p.age += dt; + + if (p.age >= p.maxAge || p.color.a <= 0.0f) { + p = emitter.particles.back(); + emitter.particles.pop_back(); + continue; + } + + p.velocity += emitter.gravity * dt; + p.velocity *= (1.0f - emitter.airResistance * dt); + + glm::vec3 nextPos = p.position + p.velocity * dt; + if (emitter.hasCollision && nextPos.y < 0.0f) { + p.velocity.y *= -emitter.bounceFactor; + p.velocity.x *= 0.8f; + nextPos.y = 0.01f; + } + p.position = nextPos; + + p.size += emitter.sizeGrowth * dt; + p.color += emitter.colorFade * dt; + + ++i; + } + + if (emitter.isActive) { + emitter.spawnAccumulator += dt; + float spawnInterval = 1.0f / emitter.spawnRate; + + while (emitter.spawnAccumulator >= spawnInterval) { + if (emitter.particles.size() < emitter.maxParticles) + SpawnParticle(emitter); + emitter.spawnAccumulator -= spawnInterval; + } + } + }); +} + +void ParticlesSystem::SpawnParticle(ECSComponent::ParticleEmitter& emitter) +{ + static thread_local std::mt19937 gen(std::random_device{}()); + std::uniform_real_distribution dist(0.0f, 1.0f); + + ECSComponent::Particle p{}; + p.position = emitter.emitterLocation; + p.velocity.x = emitter.minStartVelocity.x + dist(gen) * (emitter.maxStartVelocity.x - emitter.minStartVelocity.x); + p.velocity.y = emitter.minStartVelocity.y + dist(gen) * (emitter.maxStartVelocity.y - emitter.minStartVelocity.y); + p.velocity.z = emitter.minStartVelocity.z + dist(gen) * (emitter.maxStartVelocity.z - emitter.minStartVelocity.z); + p.color = glm::vec4(1.0f); + p.size = 0.1f; + p.age = 0.0f; + p.maxAge = emitter.particleLifetime; + + emitter.particles.push_back(p); +} + +// ---- Render ---------------------------------------------------------------- + +void ParticlesSystem::Render(ECWorld& world) +{ + // Collect instance data for every live particle across all emitters + std::vector instances; + instances.reserve(4096); + + const glm::vec3 camPos{ + static_cast(Global.pCamera.Pos.x), + static_cast(Global.pCamera.Pos.y), + static_cast(Global.pCamera.Pos.z) + }; + + world.Each( + entt::exclude, + [&](entt::entity, ECSComponent::ParticleEmitter& emitter) + { + for (const auto& p : emitter.particles) { + if (p.color.a <= 0.0f) continue; + instances.push_back({ + p.position - camPos, // camera-relative world position + std::max(p.size, 0.01f), + p.color + }); + } + }); + + if (instances.empty()) return; + + // Lazy-init OpenGL resources + if (!m_renderState) + m_renderState = std::make_unique(); + + auto& rs = *m_renderState; + + // Grow VBO if needed + if (!rs.instanceBuffer) + rs.instanceBuffer.emplace(); + + if (instances.size() > rs.bufferCapacity) { + rs.bufferCapacity = instances.size() + 1024; // headroom + rs.instanceBuffer->allocate( + gl::buffer::ARRAY_BUFFER, + static_cast(rs.bufferCapacity * sizeof(GPUInstanceData)), + GL_DYNAMIC_DRAW); + // VAO needs rebuild after buffer reallocation + rs.vao.reset(); + } + + rs.instanceBuffer->upload( + gl::buffer::ARRAY_BUFFER, + instances.data(), 0, + static_cast(instances.size() * sizeof(GPUInstanceData))); + + // Build VAO once (or after buffer realloc) + if (!rs.vao) { + rs.vao.emplace(); + constexpr int stride = sizeof(GPUInstanceData); + rs.vao->setup_attrib(*rs.instanceBuffer, 0, 3, GL_FLOAT, stride, 0); // position + glVertexAttribDivisor(0, 1); + rs.vao->setup_attrib(*rs.instanceBuffer, 1, 1, GL_FLOAT, stride, 12); // size + glVertexAttribDivisor(1, 1); + rs.vao->setup_attrib(*rs.instanceBuffer, 2, 4, GL_FLOAT, stride, 16); // color + glVertexAttribDivisor(2, 1); + rs.vao->unbind(); + rs.instanceBuffer->unbind(gl::buffer::ARRAY_BUFFER); + } + + // Load shader once + if (!rs.shader) { + try { + gl::shader vert("ecs_particle.vert"); + gl::shader frag("ecs_particle.frag"); + rs.shader = std::unique_ptr(new gl::program({vert, frag})); + } catch (const gl::shader_exception& e) { + WriteLog("[ECS] ParticlesSystem: shader compile failed: " + std::string(e.what())); + return; + } + } + + // Render + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + + rs.shader->bind(); + rs.vao->bind(); + glDrawArraysInstanced(GL_TRIANGLES, 0, 6, static_cast(instances.size())); + rs.vao->unbind(); + gl::program::unbind(); + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); +} diff --git a/entitysystem/systems/ParticlesSystem.h b/entitysystem/systems/ParticlesSystem.h new file mode 100644 index 00000000..13c5a05f --- /dev/null +++ b/entitysystem/systems/ParticlesSystem.h @@ -0,0 +1,35 @@ +// +// Created by Daniu +// + +#ifndef EU07_PARTICLESSYSTEM_H +#define EU07_PARTICLESSYSTEM_H + +#include "BaseSystem.h" +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/RenderComponents.h" + +class ParticlesSystem : public BaseSystem +{ +public: + ParticlesSystem(); + ~ParticlesSystem(); + void Update(ECWorld& world, float dt) override; + void Render(ECWorld& world) override; + +private: + void SpawnParticle(ECSComponent::ParticleEmitter& emitter); + + // Per-instance data uploaded to GPU (one entry per live particle) + struct GPUInstanceData { + glm::vec3 position; // camera-relative world position + float size; + glm::vec4 color; + }; + + // OpenGL resources — allocated lazily on first Render() call + struct RenderState; + std::unique_ptr m_renderState; +}; + +#endif // EU07_PARTICLESSYSTEM_H diff --git a/entitysystem/systems/SoundSystem.cpp b/entitysystem/systems/SoundSystem.cpp new file mode 100644 index 00000000..33fd648c --- /dev/null +++ b/entitysystem/systems/SoundSystem.cpp @@ -0,0 +1,26 @@ +#include "stdafx.h" +#include "SoundSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" + +void SoundSystem::Update(ECWorld& world, float dt) +{ + world.Each( + entt::exclude, + [](entt::entity, + ECSComponent::SoundComponent& sc, + ECSComponent::Transform& transform) + { + sc.sound.offset( glm::vec3( transform.Position ) ); + sc.sound.gain( sc.volume ); + sc.sound.pitch( sc.pitch ); + + if (sc.isPlaying && !sc.sound.is_playing()) { + int flags = sc.loop ? sound_flags::looping : 0; + sc.sound.play( flags ); + } else if (!sc.isPlaying && sc.sound.is_playing()) { + sc.sound.stop(); + } + }); +} diff --git a/entitysystem/systems/SoundSystem.h b/entitysystem/systems/SoundSystem.h new file mode 100644 index 00000000..6d2e0e2d --- /dev/null +++ b/entitysystem/systems/SoundSystem.h @@ -0,0 +1,11 @@ +#pragma once + +#include "BaseSystem.h" + +// Drives SoundComponent: starts/stops sound_source playback and keeps +// position, volume and pitch in sync with the entity's Transform. +class SoundSystem : public BaseSystem +{ +public: + void Update(ECWorld& world, float dt) override; +}; diff --git a/entitysystem/systems/SystemManager.cpp b/entitysystem/systems/SystemManager.cpp index 35d7cd51..0bb4ebf6 100644 --- a/entitysystem/systems/SystemManager.cpp +++ b/entitysystem/systems/SystemManager.cpp @@ -27,7 +27,12 @@ void SystemManager::Destroy(ECWorld& world) void SystemManager::Update(ECWorld& world, float dt) { - for (auto& system : m_systems) system->Update(world, dt); } + +void SystemManager::Render(ECWorld& world) +{ + for (auto& system : m_systems) + system->Render(world); +} diff --git a/entitysystem/systems/SystemManager.h b/entitysystem/systems/SystemManager.h index fd26be3e..06b5e862 100644 --- a/entitysystem/systems/SystemManager.h +++ b/entitysystem/systems/SystemManager.h @@ -36,6 +36,7 @@ public: void Destroy(ECWorld& world); void Update(ECWorld& world, float dt); + void Render(ECWorld& world); private: std::vector> m_systems; diff --git a/entitysystem/systems/VehicleSyncSystem.cpp b/entitysystem/systems/VehicleSyncSystem.cpp new file mode 100644 index 00000000..5afe7c01 --- /dev/null +++ b/entitysystem/systems/VehicleSyncSystem.cpp @@ -0,0 +1,30 @@ +#include "stdafx.h" +#include "VehicleSyncSystem.h" + +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "vehicle/DynObj.h" + +void VehicleSyncSystem::Update(ECWorld& world, float dt) +{ + world.Each( + entt::exclude, + [](entt::entity, + ECSComponent::VehicleRef& ref, + ECSComponent::Transform& transform) + { + if (!ref.vehicle) return; + + auto const &pos = ref.vehicle->GetPosition(); + transform.Position = glm::dvec3(pos.x, pos.y, pos.z); + + // Extract rotation from the vehicle's render matrix (column vectors = basis) + auto const *m = ref.vehicle->mMatrix.getArray(); + // mMatrix is column-major 4x4; extract upper-left 3x3 into glm + glm::mat3 rot( + m[0], m[1], m[2], + m[4], m[5], m[6], + m[8], m[9], m[10]); + transform.Rotation = glm::quat_cast(rot); + }); +} diff --git a/entitysystem/systems/VehicleSyncSystem.h b/entitysystem/systems/VehicleSyncSystem.h new file mode 100644 index 00000000..1906f3f1 --- /dev/null +++ b/entitysystem/systems/VehicleSyncSystem.h @@ -0,0 +1,11 @@ +#pragma once + +#include "BaseSystem.h" + +// Keeps ECS Transform in sync with the vehicle's actual world position each frame. +// Must run before all other systems that read Transform (e.g. LOD, Sound, Hierarchy). +class VehicleSyncSystem : public BaseSystem +{ +public: + void Update(ECWorld& world, float dt) override; +}; diff --git a/rendering/lightarray.h b/rendering/lightarray.h index 670cbea8..8e7b3cfe 100644 --- a/rendering/lightarray.h +++ b/rendering/lightarray.h @@ -38,6 +38,17 @@ public: // types typedef std::vector lightrecord_array; + struct free_light_record { + glm::dvec3 position; + glm::vec3 direction { 0.f, -1.f, 0.f }; + glm::vec3 color { 1.f, 1.f, 1.f }; + float intensity { 1.0f }; + float range { 25.0f }; + float inner_cutoff { 0.9659f }; // cos(15°) + float outer_cutoff { 0.9063f }; // cos(25°) + }; + // members lightrecord_array data; + std::vector free_lights; }; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 44a0c7a5..3b968159 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "simulation/simulationtime.h" #include "application/application.h" +#include "entitysystem/ECScene.h" #include "model/AnimModel.h" #include "rendering/opengl33geometrybank.h" #include "rendering/screenshot.h" @@ -867,7 +868,7 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode) setup_drawing(true); Render_Alpha(simulation::Region); - // particles + // particles + ECS effects Render_particles(); // precipitation; done at end, only before cab render @@ -3578,6 +3579,10 @@ void opengl33_renderer::Render_particles() Bind_Texture(0, m_smoketexture); m_particlerenderer.update(m_renderpass.pass_camera); m_renderpass.draw_stats.particles = m_particlerenderer.render(); + + // ECS particle, line and effect rendering — runs here so matrices/UBOs are set up + if (auto *scene = Application.sceneManager.CurrentScene()) + scene->Render(); } void opengl33_renderer::Render_vr_models() @@ -4707,6 +4712,31 @@ void opengl33_renderer::Update_Lights(light_array &Lights) ++light_i; ++renderlight; } + + // fill free ECS SpotLight data + for (auto const &fl : Lights.free_lights) + { + if (light_i >= gl::MAX_LIGHTS) break; + auto const lightoffset = glm::vec3{ fl.position - camera }; + if (glm::length(lightoffset) > 1000.f) continue; + + gl::light_element_ubs *light = &light_ubs.lights[light_i]; + light->pos = mv * glm::vec4(lightoffset, 1.0f); + light->dir = mv * glm::vec4(fl.direction, 0.0f); + light->type = gl::light_element_ubs::SPOT; + light->in_cutoff = fl.inner_cutoff; + light->out_cutoff = fl.outer_cutoff; + light->color = fl.color * fl.intensity; + light->linear = 4.5f / fl.range; + light->quadratic = 75.0f / (fl.range * fl.range); + light->ambient = 0.0f; + light->intensity = fl.intensity; + light->headlight_projection = glm::mat4(0.f); + light->headlight_weights = glm::vec4(0.f); + + ++light_i; + } + light_ubs.lights_count = light_i; // fill sunlight data light_ubs.ambient = m_sunlight.ambient * m_sunlight.factor;// *simulation::Environment.light_intensity(); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 04ead7f0..11655dc5 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -130,6 +130,8 @@ class opengl33_renderer : public gfx_renderer { // draws supplied geometry handles void Draw_Geometry(std::vector::iterator begin, std::vector::iterator end); void Draw_Geometry(const gfx::geometrybank_handle &handle); + // draws a geometry chunk, uploading the current modelview matrix first + void draw(gfx::geometry_handle const &handle) override; // material methods void Bind_Material_Shadow(material_handle const Material); void Update_AnimModel(TAnimModel *model); @@ -289,7 +291,6 @@ class opengl33_renderer : public gfx_renderer { bool init_viewport(viewport_config &vp); - void draw(const gfx::geometry_handle &handle); void draw(std::vector::iterator begin, std::vector::iterator end); void draw_debug_ui(); diff --git a/rendering/renderer.h b/rendering/renderer.h index 89eb7c80..90e0d52f 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -66,6 +66,8 @@ public: virtual void Bind_Texture( std::size_t const Unit, texture_handle const Texture ) = 0; virtual auto Texture( texture_handle const Texture ) -> ITexture & = 0; virtual auto Texture( texture_handle const Texture ) const -> ITexture const & = 0; + // draw a geometry chunk with the current transform state (no-op in backends that don't support it) + virtual void draw( gfx::geometry_handle const & ) {} // utility methods virtual void Pick_Control_Callback( std::function Callback ) = 0; virtual void Pick_Node_Callback( std::function Callback ) = 0; diff --git a/shaders/ecs_billboard.frag b/shaders/ecs_billboard.frag new file mode 100644 index 00000000..815fb54a --- /dev/null +++ b/shaders/ecs_billboard.frag @@ -0,0 +1,28 @@ +in vec2 f_uv; +in vec4 f_pos; + +#texture (tex1, 0, sRGB_A) +uniform sampler2D tex1; + +#include +#include +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec4 col = texture(tex1, f_uv); + if (col.a < 0.01) discard; +#if POSTFX_ENABLED + out_color = vec4(apply_fog(col.rgb), col.a); +#else + out_color = tonemap(vec4(apply_fog(col.rgb), col.a)); +#endif +#if MOTIONBLUR_ENABLED + out_motion = vec4(0.0); +#endif +} diff --git a/shaders/ecs_billboard.vert b/shaders/ecs_billboard.vert new file mode 100644 index 00000000..989c87b7 --- /dev/null +++ b/shaders/ecs_billboard.vert @@ -0,0 +1,30 @@ +// ECS billboard instanced vertex shader. +// Per-instance: camera-relative position and size. +// UVs are generated per-corner from gl_VertexID. +layout(location = 0) in vec3 i_position; +layout(location = 1) in float i_size; + +out vec2 f_uv; +out vec4 f_pos; + +#include + +const vec2 corners[6] = vec2[6]( + vec2(-0.5, -0.5), vec2( 0.5, -0.5), vec2( 0.5, 0.5), + vec2(-0.5, -0.5), vec2( 0.5, 0.5), vec2(-0.5, 0.5) +); +const vec2 uvs[6] = vec2[6]( + vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), + vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0) +); + +void main() +{ + int idx = gl_VertexID; + vec3 right = vec3(inv_view[0]); + vec3 up = vec3(inv_view[1]); + vec3 worldPos = i_position + (right * corners[idx].x + up * corners[idx].y) * i_size; + f_pos = modelview * vec4(worldPos, 1.0); + gl_Position = projection * f_pos; + f_uv = uvs[idx]; +} diff --git a/shaders/ecs_line.frag b/shaders/ecs_line.frag new file mode 100644 index 00000000..f8f48d15 --- /dev/null +++ b/shaders/ecs_line.frag @@ -0,0 +1,24 @@ +in vec3 f_color; +in vec4 f_pos; + +#include +#include +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec3 col = f_color; +#if POSTFX_ENABLED + out_color = vec4(apply_fog(col), 1.0); +#else + out_color = tonemap(vec4(apply_fog(col), 1.0)); +#endif +#if MOTIONBLUR_ENABLED + out_motion = vec4(0.0); +#endif +} diff --git a/shaders/ecs_particle.frag b/shaders/ecs_particle.frag new file mode 100644 index 00000000..66b04c83 --- /dev/null +++ b/shaders/ecs_particle.frag @@ -0,0 +1,24 @@ +in vec4 f_color; +in vec4 f_pos; + +#include +#include +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec4 col = f_color; +#if POSTFX_ENABLED + out_color = vec4(apply_fog(col.rgb), col.a); +#else + out_color = tonemap(vec4(apply_fog(col.rgb), col.a)); +#endif +#if MOTIONBLUR_ENABLED + out_motion = vec4(0.0); +#endif +} diff --git a/shaders/ecs_particle.vert b/shaders/ecs_particle.vert new file mode 100644 index 00000000..25d38fb9 --- /dev/null +++ b/shaders/ecs_particle.vert @@ -0,0 +1,34 @@ +// ECS particle instanced billboard vertex shader. +// Per-instance data: camera-relative world position, size, color. +// Billboard corners are generated via gl_VertexID (no geometry shader needed). +layout(location = 0) in vec3 i_position; // camera-relative world position +layout(location = 1) in float i_size; +layout(location = 2) in vec4 i_color; + +out vec4 f_color; +out vec4 f_pos; + +#include + +// Two-triangle billboard quad (CCW winding) +const vec2 corners[6] = vec2[6]( + vec2(-0.5, -0.5), vec2( 0.5, -0.5), vec2( 0.5, 0.5), + vec2(-0.5, -0.5), vec2( 0.5, 0.5), vec2(-0.5, 0.5) +); + +void main() +{ + vec2 corner = corners[gl_VertexID]; + + // inv_view is the inverse of the rotation-only view matrix. + // Its columns are the camera's right, up, forward vectors in world space. + vec3 right = vec3(inv_view[0]); + vec3 up = vec3(inv_view[1]); + + // Expand billboard in world space around the camera-relative centre. + vec3 worldPos = i_position + (right * corner.x + up * corner.y) * i_size; + + f_pos = modelview * vec4(worldPos, 1.0); + gl_Position = projection * f_pos; + f_color = i_color; +} diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index a8a9342c..c4593ac4 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -430,28 +430,29 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch >> nodedata.name >> nodedata.type; - ECWorld& world = Application.sceneManager.CurrentScene()->World(); - + ECScene* currentScene = Application.sceneManager.CurrentScene(); + if (currentScene == nullptr) + return; + ECWorld& world = currentScene->World(); + entt::entity entity = world.CreateEntity(); - - auto transformComponent = world.AddComponent(entity); + + auto& transformComponent = world.AddComponent(entity); transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top(); transformComponent.Rotation = Scratchpad.location.rotation; - //nodeTransform.Scale = Scratchpad.location.scale; - // auto LODComponent = world.AddComponent(entity); - // LODComponent.RangeMax = nodedata.range_max; - // LODComponent.RangeMin = nodedata.range_min; - - if( nodedata.name == "none" ) - { - nodedata.name.clear(); - } - else - { - world.AddComponent(entity).Name = nodedata.name; - + if (nodedata.range_max > 0.0 || nodedata.range_min >= 0.0) { + auto& lodComponent = world.AddComponent(entity); + lodComponent.RangeMin = nodedata.range_min; + lodComponent.RangeMax = nodedata.range_max; } + + if( nodedata.name == "none" ) { + nodedata.name.clear(); + } else { + auto& id = world.AddComponent(entity); + id.Name = nodedata.name; + } // type-based deserialization. not elegant but it'll do if( nodedata.type == "dynamic" ) { @@ -462,25 +463,34 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch // if( vehicle->mdModel != nullptr ) { for( auto const &smokesource : vehicle->mdModel->smoke_sources() ) { - Particles.insert( - smokesource.first, - vehicle, - smokesource.second ); + auto& particleComponent = world.AddComponent(entity); + particleComponent.emitterLocation = smokesource.second; + particleComponent.isActive = true; } } - if( false == simulation::Vehicles.insert( vehicle ) ) { - + if( !Vehicles.insert( vehicle ) ) { ErrorLog( "Bad scenario: duplicate vehicle name \"" + vehicle->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); } - auto velocityComponent = world.AddComponent(entity); - velocityComponent.Value = glm::dvec3(0.0); + world.AddComponent(entity).Value = glm::dvec3(0.0); - if( ( vehicle->MoverParameters->CategoryFlag == 1 ) // trains only - && ( ( ( vehicle->LightList( end::front ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) - || ( ( vehicle->LightList( end::rear ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) ) ) { + // link ECS entity back to the live vehicle so VehicleSyncSystem can track it + world.AddComponent(entity).vehicle = vehicle; + + // headlight spotlights + bool const hasFrontLights = ( vehicle->LightList( end::front ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0; + bool const hasRearLights = ( vehicle->LightList( end::rear ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0; + if( ( vehicle->MoverParameters->CategoryFlag == 1 ) && ( hasFrontLights || hasRearLights ) ) { simulation::Lights.insert( vehicle ); + auto& spotlight = world.AddComponent(entity); + spotlight.Color = glm::vec3(1.0f, 0.95f, 0.85f); + spotlight.Intensity = 1.0f; + spotlight.Range = 150.0f; + spotlight.InnerAngle = 8.0f; + spotlight.OuterAngle = 20.0f; + spotlight.CastShadows = false; + spotlight.Enabled = hasFrontLights; } } else if( nodedata.type == "track" ) { @@ -579,20 +589,27 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch scene::Groups.insert( scene::Groups.handle(), instance ); simulation::Region->insert( instance ); - ECWorld& world = Application.sceneManager.CurrentScene()->World(); - - entt::entity entity = world.FindEntityByName(instance->name()); - - if(entity != entt::null) { - - auto meshComponent = world.AddComponent(entity); - //meshComponent.meshHandle = instance->Model()->MeshHandle(); - meshComponent.visible = true; + { + entt::entity modelEntity = world.FindEntityByName(instance->name()); + if (modelEntity != entt::null) { + auto& meshComponent = world.AddComponent(modelEntity); + meshComponent.modelInstance = instance; + meshComponent.visible = true; - auto* transformComponent = world.GetComponent(entity); - transformComponent->Position = {instance->location().x, instance->location().y, instance->location().z}; - transformComponent->Rotation = {instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f}; - //transformComponent->Scale = {instance->scale().x, instance->scale().y, instance->scale().z}; + if (auto* t = world.GetComponent(modelEntity)) { + t->Position = { instance->location().x, instance->location().y, instance->location().z }; + t->Rotation = { instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f }; + } + + // smoke sources on static models + if (instance->Model() != nullptr) { + for (auto const &smokesource : instance->Model()->smoke_sources()) { + auto& emitter = world.AddComponent(modelEntity); + emitter.emitterLocation = smokesource.second; + emitter.isActive = true; + } + } + } } scene::basic_node *hierarchy_node = instance; @@ -675,13 +692,14 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch auto *sound { deserialize_sound( Input, Scratchpad, nodedata ) }; - auto soundComponent = world.AddComponent(entity); - - soundComponent.sound = *sound; - soundComponent.volume = 1; - soundComponent.pitch = 1; - soundComponent.range = sound->range(); + auto& soundComponent = world.AddComponent(entity); + soundComponent.sound.copy_sounds(*sound); + soundComponent.volume = sound->gain(); + soundComponent.pitch = 1.0f; + soundComponent.range = sound->range(); + soundComponent.loop = false; soundComponent.isPlaying = sound->is_playing(); + soundComponent.playOnStart = false; if( false == simulation::Sounds.insert( sound ) ) { ErrorLog( "Bad scenario: duplicate sound node name \"" + sound->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );