16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 00:49:19 +02:00
This commit is contained in:
2026-05-24 21:48:05 +02:00
parent 5b51188dfe
commit 2c599cb419
46 changed files with 2034 additions and 85 deletions

View File

@@ -38,6 +38,7 @@ set(VERSION_STRING "${VERSION_YEAR}.${VERSION_MONTH}.${VERSION_DAY}")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
file(GLOB HEADERS "*.h" file(GLOB HEADERS "*.h"
"DevConsole/*.h"
"Console/*.h" "Console/*.h"
"McZapkie/*.h" "McZapkie/*.h"
"gl/*.h" "gl/*.h"
@@ -200,6 +201,8 @@ set(SOURCES
"network/manager.cpp" "network/manager.cpp"
"network/backend/asio.cpp" "network/backend/asio.cpp"
"DevConsole/devconsole.cpp"
"widgets/vehiclelist.cpp" "widgets/vehiclelist.cpp"
"widgets/map.cpp" "widgets/map.cpp"
"widgets/map_objects.cpp" "widgets/map_objects.cpp"
@@ -410,7 +413,25 @@ configure_file(${CMAKE_SOURCE_DIR}/eu07.rc.in
if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
set(SOURCES ${SOURCES} ${CMAKE_BINARY_DIR}/eu07.rc) 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") add_compile_options("/utf-8")
endif() endif()

379
DevConsole/devconsole.cpp Normal file
View File

@@ -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 <sstream>
#include <algorithm>
#include <cctype>
#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<std::string> 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<std::string> console_panel::completions_for(const std::string &prefix) const
{
std::string lp = str_tolower(prefix);
std::vector<std::string> 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<CallbackUserData *>(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<int>(begin - data->Buf),
static_cast<int>(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<int>(begin - data->Buf),
static_cast<int>(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<int>(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<int>(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

100
DevConsole/devconsole.h Normal file
View File

@@ -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 <string>
#include <vector>
#include <functional>
#include <unordered_map>
#include <deque>
#include "application/uilayer.h"
struct ImGuiInputTextCallbackData;
namespace dev {
// Parsed command arguments — name of the command is args[0]
using args_t = std::vector<std::string>;
// Handler signature: receives all tokens including the command name at [0]
using command_handler_t = std::function<void(const args_t &)>;
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<std::string> 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<std::string, console_command> m_commands;
std::deque<log_entry> m_log;
std::deque<std::string> 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

View File

@@ -11,12 +11,17 @@ http://mozilla.org/MPL/2.0/.
#include "application/driveruilayer.h" #include "application/driveruilayer.h"
#include "utilities/Globals.h" #include "utilities/Globals.h"
#include "utilities/utilities.h"
#include "application/application.h" #include "application/application.h"
#include "utilities/translation.h" #include "utilities/translation.h"
#include "simulation/simulation.h" #include "simulation/simulation.h"
#include "vehicle/Train.h" #include "vehicle/Train.h"
#include "model/AnimModel.h" #include "model/AnimModel.h"
#include "rendering/renderer.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() driver_ui::driver_ui()
{ {
@@ -36,6 +41,7 @@ driver_ui::driver_ui()
add_external_panel(&m_mappanel); add_external_panel(&m_mappanel);
add_external_panel(&m_logpanel); add_external_panel(&m_logpanel);
add_external_panel(&m_perfgraphpanel); add_external_panel(&m_perfgraphpanel);
add_external_panel(&dev::Console);
add_external_panel(&m_cameraviewpanel); add_external_panel(&m_cameraviewpanel);
m_logpanel.is_open = false; m_logpanel.is_open = false;
@@ -65,6 +71,302 @@ driver_ui::driver_ui()
m_trainingcardpanel.is_open = true; m_trainingcardpanel.is_open = true;
m_vehiclelist.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<unsigned>(entity));
if (auto *id = world.GetComponent<ECSComponent::Identification>(entity))
name = id->Name.ToString();
if (!filter.empty() && name.find(filter) == std::string::npos)
continue;
dev::Console.print(" [" + std::to_string(static_cast<unsigned>(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 <name>",
[](const dev::args_t &args) {
if (args.size() < 2) { dev::Console.print_error("Usage: ecs.info <name>"); 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<ECSComponent::Transform>(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<ECSComponent::Velocity>(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<ECSComponent::MeshRenderer>(entity))
dev::Console.print(" MeshRenderer: yes");
if (world.HasComponent<ECSComponent::ParticleEmitter>(entity))
dev::Console.print(" ParticleEmitter: yes");
if (auto *id = world.GetComponent<ECSComponent::Identification>(entity))
dev::Console.print(" ID: " + id->Name.ToString() + " #" + std::to_string(id->Id));
});
con.register_command("ecs.setvel", "Set entity velocity: ecs.setvel <name> <x> <y> <z>",
[](const dev::args_t &args) {
if (args.size() < 5) { dev::Console.print_error("Usage: ecs.setvel <name> <x> <y> <z>"); 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<ECSComponent::Velocity>(entity)) {
v->Value = vel;
dev::Console.print_ok("Velocity set");
} else {
world.AddComponent<ECSComponent::Velocity>(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 <name>",
[](const dev::args_t &args) {
if (args.size() < 2) { dev::Console.print_error("Usage: ecs.kill <name>"); 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 <name> <x> <y> <z>",
[](const dev::args_t &args) {
if (args.size() < 5) { dev::Console.print_error("Usage: ecs.setpos <name> <x> <y> <z>"); 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<ECSComponent::Transform>(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 <name> <x> <y> <z>",
[](const dev::args_t &args) {
if (args.size() < 5) { dev::Console.print_error("Usage: ecs.spawn <name> <x> <y> <z>"); 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<ECSComponent::Identification>(entity);
id.Name = args[1];
auto &t = world.AddComponent<ECSComponent::Transform>(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 <fragment>",
[](const dev::args_t &args) {
if (args.size() < 2) { dev::Console.print_error("Usage: ecs.find <fragment>"); 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<ECSComponent::Identification>(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<unsigned>(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 <name>",
[](const dev::args_t &args) {
if (args.size() < 2) { dev::Console.print_error("Usage: ecs.disable <name>"); 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<ECSComponent::Disabled>(entity))
world.AddComponent<ECSComponent::Disabled>(entity);
dev::Console.print_ok("Disabled: " + args[1]);
});
con.register_command("ecs.enable", "Re-enable a disabled entity: ecs.enable <name>",
[](const dev::args_t &args) {
if (args.size() < 2) { dev::Console.print_error("Usage: ecs.enable <name>"); 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<ECSComponent::Disabled>(entity);
dev::Console.print_ok("Enabled: " + args[1]);
});
con.register_command("ecs.addparticles", "Add a particle emitter to an entity: ecs.addparticles <name>",
[](const dev::args_t &args) {
if (args.size() < 2) { dev::Console.print_error("Usage: ecs.addparticles <name>"); 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<ECSComponent::ParticleEmitter>(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<ECSComponent::Transform>(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() void driver_ui::render_menu_contents()

View File

@@ -53,6 +53,9 @@ private:
// render() subclass details // render() subclass details
void void
render_() override; render_() override;
// registers driver-specific console commands
void
register_driver_commands();
// members // members
drivingaid_panel m_aidpanel { "Driving Aid", false }; drivingaid_panel m_aidpanel { "Driving Aid", false };

View File

@@ -24,6 +24,7 @@ http://mozilla.org/MPL/2.0/.
#include "entitysystem/components/RenderComponents.h" #include "entitysystem/components/RenderComponents.h"
#include "imgui/imgui_impl_glfw.h" #include "imgui/imgui_impl_glfw.h"
#include "DevConsole/devconsole.h"
GLFWwindow *ui_layer::m_window{nullptr}; GLFWwindow *ui_layer::m_window{nullptr};
ImGuiIO *ui_layer::m_imguiio{nullptr}; ImGuiIO *ui_layer::m_imguiio{nullptr};
@@ -105,6 +106,8 @@ ui_layer::ui_layer()
if (Global.loading_log) if (Global.loading_log)
add_external_panel(&m_logpanel); add_external_panel(&m_logpanel);
m_logpanel.size = {700, 400}; m_logpanel.size = {700, 400};
add_external_panel(&dev::Console);
} }
ui_layer::~ui_layer() {} ui_layer::~ui_layer() {}
@@ -319,6 +322,15 @@ bool ui_layer::on_key(int const Key, int const Action)
return true; 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) if (Key == GLFW_KEY_F9)
{ {
m_logpanel.is_open = !m_logpanel.is_open; m_logpanel.is_open = !m_logpanel.is_open;
@@ -744,6 +756,7 @@ void ui_layer::render_menu_contents()
if (ImGui::BeginMenu(STR_C("Windows"))) if (ImGui::BeginMenu(STR_C("Windows")))
{ {
ImGui::MenuItem(STR_C("Log"), "F9", &m_logpanel.is_open); ImGui::MenuItem(STR_C("Log"), "F9", &m_logpanel.is_open);
ImGui::MenuItem(STR_C("Developer Console"), "`", &dev::Console.is_open);
if (DebugModeFlag) if (DebugModeFlag)
{ {
ImGui::MenuItem(STR_C("ImGui Demo"), nullptr, &m_imgui_demo); ImGui::MenuItem(STR_C("ImGui Demo"), nullptr, &m_imgui_demo);

View File

@@ -36,14 +36,21 @@ void ECScene::Unload()
void ECScene::Update(float dt) void ECScene::Update(float dt)
{ {
if (!m_loaded){ if (!m_loaded)
return; return;
}
m_systems.Update(m_world, dt); m_systems.Update(m_world, dt);
OnUpdate(dt); OnUpdate(dt);
} }
void ECScene::Render()
{
if (!m_loaded)
return;
m_systems.Render(m_world);
}
ECWorld& ECScene::World() ECWorld& ECScene::World()
{ {

View File

@@ -21,6 +21,7 @@ public:
void Load(); void Load();
void Unload(); void Unload();
void Update(float dt); void Update(float dt);
void Render();
ECWorld& World(); ECWorld& World();
const ECWorld& World() const; const ECWorld& World() const;

View File

@@ -52,3 +52,9 @@ const entt::registry& ECWorld::Registry() const
{ {
return m_registry; return m_registry;
} }
ECWorld& GetComponentSystem()
{
static ECWorld instance;
return instance;
}

View File

@@ -66,6 +66,24 @@ public:
}, components); }, components);
} }
} }
// Same as Each but skips entities that have any of the Excluded components.
// Usage: world.Each<A, B>(entt::exclude<Disabled>, lambda)
template<typename... Components, typename... Excluded, typename Func>
void Each(entt::exclude_t<Excluded...>, Func&& func)
{
auto view = m_registry.view<Components...>(entt::exclude<Excluded...>);
for (auto entity : view)
{
auto components = std::forward_as_tuple(view.get<Components>(entity)...);
std::apply([&](Components&... comps)
{
func(entity, comps...);
}, components);
}
}
std::vector<entt::entity> GetEntities() const std::vector<entt::entity> GetEntities() const
{ {
@@ -96,11 +114,16 @@ T& ECWorld::AddComponent(entt::entity entity, Args&&... args)
"Component must be constructible with provided arguments"); "Component must be constructible with provided arguments");
if (m_registry.all_of<T>(entity)) if (m_registry.all_of<T>(entity))
{ m_registry.replace<T>(entity, std::forward<Args>(args)...);
return m_registry.replace<T>(entity, std::forward<Args>(args)...); else
} m_registry.emplace<T>(entity, std::forward<Args>(args)...);
return m_registry.emplace<T>(entity, std::forward<Args>(args)...); if constexpr (std::is_empty_v<T>) {
static T dummy{};
return dummy;
} else {
return m_registry.get<T>(entity);
}
} }
template<typename T> template<typename T>

View File

@@ -16,8 +16,6 @@ SceneManager::~SceneManager()
void SceneManager::SetScene(std::unique_ptr<ECScene> scene) void SceneManager::SetScene(std::unique_ptr<ECScene> scene)
{ {
m_pendingScene = std::move(scene); m_pendingScene = std::move(scene);
if (m_currentScene)
m_currentScene->Load();
} }
void SceneManager::Update(float dt) void SceneManager::Update(float dt)

View File

@@ -8,10 +8,12 @@
#include "glm/vec3.hpp" #include "glm/vec3.hpp"
#include "glm/gtc/quaternion.hpp" #include "glm/gtc/quaternion.hpp"
#include "registry/FName.h" #include "registry/FName.h"
#include "utils/uuid.hpp" #include "utilities/uuid.hpp"
#include <cstdint> #include <cstdint>
#include <string> #include <string>
class TDynamicObject;
namespace ECSComponent namespace ECSComponent
{ {
///< summary> ///< summary>
@@ -45,6 +47,15 @@ struct Identification
struct Parent struct Parent
{ {
entt::entity value{entt::null}; 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 struct SoundComponent

View File

@@ -4,26 +4,23 @@
#include "registry/FName.h" #include "registry/FName.h"
#include "rendering/geometrybank.h" #include "rendering/geometrybank.h"
class TAnimModel;
namespace ECSComponent namespace ECSComponent
{ {
/// <summary> /// <summary>
/// Component for entities that can be rendered. /// Component for entities that can be rendered.
/// </summary> /// </summary>
/// <remarks>Currently empty
/// TODO: Add component members
/// </remarks>
struct MeshRenderer struct MeshRenderer
{ {
gfx::geometry_handle meshHandle; gfx::geometry_handle meshHandle;
TAnimModel* modelInstance = nullptr;
bool visible = true; bool visible = true;
}; };
/// <summary> /// <summary>
/// Component for entities that can cast shadows. /// Component for entities that can cast shadows.
/// </summary> /// </summary>
/// <remarks>Currently empty
/// TODO: Add component members
/// </remarks>
struct SpotLight struct SpotLight
{ {
glm::vec3 Color{ 1.0f, 1.0f, 1.0f }; glm::vec3 Color{ 1.0f, 1.0f, 1.0f };
@@ -38,16 +35,70 @@ struct SpotLight
bool Enabled = true; bool Enabled = true;
}; };
// TODO: AreaLight like blender /// <summary>
// TODO: SunLight for scene ///
///</summary>
struct AreaLight
struct ParticleEmitter
{ {
FName particleEffectPath = (""); glm::vec3 Color{ 1.0f, 1.0f, 1.0f };
bool active = true; 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<Particle> 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)
};
/// <summary>
/// Component for entities that can be rendered as decals (e.g. graffiti on wagons, dirties on wall).
/// </summary>
struct Decal struct Decal
{ {
FName decalTexturePath = (""); FName decalTexturePath = ("");
@@ -55,6 +106,10 @@ struct Decal
bool active = true; bool active = true;
}; };
/// <summary>
/// Component for entities that can be rendered as billboards (e.g. particles, sprites).
/// </summary>
struct Billboard struct Billboard
{ {
FName texturePath = (""); FName texturePath = ("");
@@ -62,6 +117,9 @@ struct Billboard
bool active = true; bool active = true;
}; };
/// <summary>
/// Component for entities that can be rendered as lines.
/// </summary>
struct Line { struct Line {
glm::vec3 start{ 0.0f }; glm::vec3 start{ 0.0f };
glm::vec3 end{ 1.0f, 0.0f, 0.0f }; glm::vec3 end{ 1.0f, 0.0f, 0.0f };

View File

@@ -4,32 +4,96 @@
#include "GameScene.h" #include "GameScene.h"
#include "entitysystem/components/BasicComponents.h" #include "entitysystem/components/BasicComponents.h"
#include "entitysystem/components/RenderComponents.h" #include "entitysystem/components/RenderComponents.h"
#include "entitysystem/systems/MovementSystem.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" #include "utilities/Logs.h"
GameScene::GameScene() = default; GameScene::GameScene() = default;
GameScene::~GameScene() = default ; GameScene::~GameScene() = default;
void GameScene::OnLoad() void GameScene::OnLoad()
{ {
m_systems.AddSystem<VehicleSyncSystem>(); // must be first — updates Transform from live vehicle
m_systems.AddSystem<MovementSystem>(); m_systems.AddSystem<MovementSystem>();
m_systems.AddSystem<HierarchySystem>();
m_systems.AddSystem<LightSystem>();
m_systems.AddSystem<ParticlesSystem>();
m_systems.AddSystem<SoundSystem>();
m_systems.AddSystem<LODSystem>();
m_systems.AddSystem<MeshRenderSystem>();
m_systems.AddSystem<LineSystem>();
m_systems.AddSystem<BillboardSystem>();
#ifdef _DEBUG
CreateTestEntities();
#endif
} }
void GameScene::OnUpdate(float dt) void GameScene::OnUpdate(float dt)
{ {
}
//auto& world = World();
//auto view = world.View<ECSComponent::Transform>();
}
void GameScene::OnUnload() void GameScene::OnUnload()
{ {
World().Clear(); 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<ECSComponent::Identification>(entity);
id.Name = "TestSmokeEmitter";
auto& transform = world.AddComponent<ECSComponent::Transform>(entity);
transform.Position = glm::dvec3(0.0, 2.0, 0.0);
auto& emitter = world.AddComponent<ECSComponent::ParticleEmitter>(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<ECSComponent::Identification>(entity);
id.Name = "TestMovingBox";
auto& transform = world.AddComponent<ECSComponent::Transform>(entity);
transform.Position = glm::dvec3(10.0, 0.5, 0.0);
auto& velocity = world.AddComponent<ECSComponent::Velocity>(entity);
velocity.Value = glm::vec3(-1.0f, 0.0f, 0.0f);
auto& lod = world.AddComponent<ECSComponent::LODController>(entity);
lod.RangeMin = 0.0;
lod.RangeMax = 200.0;
auto& mesh = world.AddComponent<ECSComponent::MeshRenderer>(entity);
mesh.visible = true;
WriteLog("[ECS] Created test entity: TestMovingBox");
}
} }

View File

@@ -19,6 +19,7 @@ public:
virtual void OnDestroy(ECWorld& world) {} virtual void OnDestroy(ECWorld& world) {}
virtual void Update(ECWorld& world, float dt) {} virtual void Update(ECWorld& world, float dt) {}
virtual void Render(ECWorld& world) {}
}; };

View File

@@ -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<gl::buffer> instanceBuffer;
std::optional<gl::vao> vao;
std::unique_ptr<gl::program> 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<std::string, std::vector<BillboardInstance>> groups;
const glm::vec3 camPos{
static_cast<float>(Global.pCamera.Pos.x),
static_cast<float>(Global.pCamera.Pos.y),
static_cast<float>(Global.pCamera.Pos.z)
};
world.Each<ECSComponent::Billboard, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[&](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<RenderState>();
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<gl::program>(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<int>(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<int>(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<GLsizei>(instances.size()));
rs.vao->unbind();
}
gl::program::unbind();
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "BaseSystem.h"
#include "rendering/renderer.h"
#include <unordered_map>
#include <string>
// 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<RenderState> m_renderState;
};

View File

@@ -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<ECSComponent::Parent, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[&](entt::entity, ECSComponent::Parent& parent, ECSComponent::Transform& childTransform)
{
if (parent.value == entt::null || !world.IsAlive(parent.value))
return;
const auto* parentTransform = world.GetComponent<ECSComponent::Transform>(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;
});
}

View File

@@ -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;
};

View File

@@ -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<ECSComponent::LODController, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[&](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<ECSComponent::MeshRenderer>(entity)) {
mesh->visible = inRange;
if (mesh->modelInstance)
mesh->modelInstance->visible(inRange);
}
});
}

View File

@@ -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;
};

View File

@@ -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<ECSComponent::SunLight>(
entt::exclude<ECSComponent::Disabled>,
[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<ECSComponent::SpotLight, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[&](entt::entity, ECSComponent::SpotLight& spot, ECSComponent::Transform& transform)
{
double dist = glm::length(camPos - transform.Position);
if (dist > static_cast<double>(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.
}

View File

@@ -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;
};

View File

@@ -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<gl::buffer> vertexBuffer;
std::optional<gl::vao> vao;
std::unique_ptr<gl::program> shader;
std::size_t bufferCapacity { 0 };
};
LineSystem::LineSystem() = default;
LineSystem::~LineSystem() = default;
void LineSystem::Render(ECWorld& world)
{
std::vector<LineVertex> vertices;
vertices.reserve(256);
const glm::vec3 camPos{
static_cast<float>(Global.pCamera.Pos.x),
static_cast<float>(Global.pCamera.Pos.y),
static_cast<float>(Global.pCamera.Pos.z)
};
world.Each<ECSComponent::Line, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[&](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<RenderState>();
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<int>(rs.bufferCapacity * sizeof(LineVertex)),
GL_DYNAMIC_DRAW);
rs.vao.reset();
}
rs.vertexBuffer->upload(
gl::buffer::ARRAY_BUFFER,
vertices.data(), 0,
static_cast<int>(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<gl::program>(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<GLsizei>(vertices.size()));
rs.vao->unbind();
gl::program::unbind();
}

View File

@@ -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<RenderState> m_renderState;
};

View File

@@ -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)
{
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "BaseSystem.h"
class MeshRenderSystem : public BaseSystem
{
public:
void Render(ECWorld& world) override;
};

View File

@@ -13,6 +13,7 @@ void MovementSystem::Update(ECWorld& world, float dt)
constexpr double damping = 0.90; // 0.0 = instant stop, 1.0 = without damping constexpr double damping = 0.90; // 0.0 = instant stop, 1.0 = without damping
world.Each<ECSComponent::Transform, ECSComponent::Velocity>( world.Each<ECSComponent::Transform, ECSComponent::Velocity>(
entt::exclude<ECSComponent::Disabled>,
[dt](auto entity, auto& transform, auto& velocity) [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) if (glm::length(velocity.Value) < 0.001)
{ {
velocity.Value = glm::dvec3(0.0); velocity.Value = glm::vec3(0.0f);
} }
} }
); );

View File

@@ -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 <algorithm>
#include <random>
// ---- OpenGL resource bundle (pimpl) ----------------------------------------
struct ParticlesSystem::RenderState
{
std::optional<gl::buffer> instanceBuffer;
std::optional<gl::vao> vao;
std::unique_ptr<gl::program> shader;
std::size_t bufferCapacity { 0 };
};
ParticlesSystem::ParticlesSystem() = default;
ParticlesSystem::~ParticlesSystem() = default;
// ---- Update ----------------------------------------------------------------
void ParticlesSystem::Update(ECWorld &world, float dt)
{
world.Each<ECSComponent::ParticleEmitter>(
entt::exclude<ECSComponent::Disabled>,
[&](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<float> 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<GPUInstanceData> instances;
instances.reserve(4096);
const glm::vec3 camPos{
static_cast<float>(Global.pCamera.Pos.x),
static_cast<float>(Global.pCamera.Pos.y),
static_cast<float>(Global.pCamera.Pos.z)
};
world.Each<ECSComponent::ParticleEmitter>(
entt::exclude<ECSComponent::Disabled>,
[&](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<RenderState>();
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<int>(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<int>(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<gl::program>(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<GLsizei>(instances.size()));
rs.vao->unbind();
gl::program::unbind();
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}

View File

@@ -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<RenderState> m_renderState;
};
#endif // EU07_PARTICLESSYSTEM_H

View File

@@ -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<ECSComponent::SoundComponent, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[](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();
}
});
}

View File

@@ -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;
};

View File

@@ -27,7 +27,12 @@ void SystemManager::Destroy(ECWorld& world)
void SystemManager::Update(ECWorld& world, float dt) void SystemManager::Update(ECWorld& world, float dt)
{ {
for (auto& system : m_systems) for (auto& system : m_systems)
system->Update(world, dt); system->Update(world, dt);
} }
void SystemManager::Render(ECWorld& world)
{
for (auto& system : m_systems)
system->Render(world);
}

View File

@@ -36,6 +36,7 @@ public:
void Destroy(ECWorld& world); void Destroy(ECWorld& world);
void Update(ECWorld& world, float dt); void Update(ECWorld& world, float dt);
void Render(ECWorld& world);
private: private:
std::vector<std::unique_ptr<BaseSystem>> m_systems; std::vector<std::unique_ptr<BaseSystem>> m_systems;

View File

@@ -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<ECSComponent::VehicleRef, ECSComponent::Transform>(
entt::exclude<ECSComponent::Disabled>,
[](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);
});
}

View File

@@ -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;
};

View File

@@ -38,6 +38,17 @@ public:
// types // types
typedef std::vector<light_record> lightrecord_array; typedef std::vector<light_record> 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 // members
lightrecord_array data; lightrecord_array data;
std::vector<free_light_record> free_lights;
}; };

View File

@@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/utilities.h" #include "utilities/utilities.h"
#include "simulation/simulationtime.h" #include "simulation/simulationtime.h"
#include "application/application.h" #include "application/application.h"
#include "entitysystem/ECScene.h"
#include "model/AnimModel.h" #include "model/AnimModel.h"
#include "rendering/opengl33geometrybank.h" #include "rendering/opengl33geometrybank.h"
#include "rendering/screenshot.h" #include "rendering/screenshot.h"
@@ -867,7 +868,7 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode)
setup_drawing(true); setup_drawing(true);
Render_Alpha(simulation::Region); Render_Alpha(simulation::Region);
// particles // particles + ECS effects
Render_particles(); Render_particles();
// precipitation; done at end, only before cab render // precipitation; done at end, only before cab render
@@ -3578,6 +3579,10 @@ void opengl33_renderer::Render_particles()
Bind_Texture(0, m_smoketexture); Bind_Texture(0, m_smoketexture);
m_particlerenderer.update(m_renderpass.pass_camera); m_particlerenderer.update(m_renderpass.pass_camera);
m_renderpass.draw_stats.particles = m_particlerenderer.render(); 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() void opengl33_renderer::Render_vr_models()
@@ -4707,6 +4712,31 @@ void opengl33_renderer::Update_Lights(light_array &Lights)
++light_i; ++light_i;
++renderlight; ++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; light_ubs.lights_count = light_i;
// fill sunlight data // fill sunlight data
light_ubs.ambient = m_sunlight.ambient * m_sunlight.factor;// *simulation::Environment.light_intensity(); light_ubs.ambient = m_sunlight.ambient * m_sunlight.factor;// *simulation::Environment.light_intensity();

View File

@@ -130,6 +130,8 @@ class opengl33_renderer : public gfx_renderer {
// draws supplied geometry handles // draws supplied geometry handles
void Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end); void Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end);
void Draw_Geometry(const gfx::geometrybank_handle &handle); 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 // material methods
void Bind_Material_Shadow(material_handle const Material); void Bind_Material_Shadow(material_handle const Material);
void Update_AnimModel(TAnimModel *model); void Update_AnimModel(TAnimModel *model);
@@ -289,7 +291,6 @@ class opengl33_renderer : public gfx_renderer {
bool init_viewport(viewport_config &vp); bool init_viewport(viewport_config &vp);
void draw(const gfx::geometry_handle &handle);
void draw(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end); void draw(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end);
void draw_debug_ui(); void draw_debug_ui();

View File

@@ -66,6 +66,8 @@ public:
virtual void Bind_Texture( std::size_t const Unit, texture_handle const Texture ) = 0; 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 ) -> ITexture & = 0;
virtual auto Texture( texture_handle const Texture ) const -> ITexture const & = 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 // utility methods
virtual void Pick_Control_Callback( std::function<void( TSubModel const *, const glm::vec2 )> Callback ) = 0; virtual void Pick_Control_Callback( std::function<void( TSubModel const *, const glm::vec2 )> Callback ) = 0;
virtual void Pick_Node_Callback( std::function<void( scene::basic_node * )> Callback ) = 0; virtual void Pick_Node_Callback( std::function<void( scene::basic_node * )> Callback ) = 0;

View File

@@ -0,0 +1,28 @@
in vec2 f_uv;
in vec4 f_pos;
#texture (tex1, 0, sRGB_A)
uniform sampler2D tex1;
#include <common>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
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
}

View File

@@ -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 <common>
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];
}

24
shaders/ecs_line.frag Normal file
View File

@@ -0,0 +1,24 @@
in vec3 f_color;
in vec4 f_pos;
#include <common>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
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
}

24
shaders/ecs_particle.frag Normal file
View File

@@ -0,0 +1,24 @@
in vec4 f_color;
in vec4 f_pos;
#include <common>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
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
}

34
shaders/ecs_particle.vert Normal file
View File

@@ -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 <common>
// 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;
}

View File

@@ -430,28 +430,29 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
>> nodedata.name >> nodedata.name
>> nodedata.type; >> 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(); entt::entity entity = world.CreateEntity();
auto transformComponent = world.AddComponent<ECSComponent::Transform>(entity); auto& transformComponent = world.AddComponent<ECSComponent::Transform>(entity);
transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top(); transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top();
transformComponent.Rotation = Scratchpad.location.rotation; transformComponent.Rotation = Scratchpad.location.rotation;
//nodeTransform.Scale = Scratchpad.location.scale;
// auto LODComponent = world.AddComponent<ECSComponent::LODController>(entity); if (nodedata.range_max > 0.0 || nodedata.range_min >= 0.0) {
// LODComponent.RangeMax = nodedata.range_max; auto& lodComponent = world.AddComponent<ECSComponent::LODController>(entity);
// LODComponent.RangeMin = nodedata.range_min; lodComponent.RangeMin = nodedata.range_min;
lodComponent.RangeMax = nodedata.range_max;
if( nodedata.name == "none" )
{
nodedata.name.clear();
}
else
{
world.AddComponent<ECSComponent::Identification>(entity).Name = nodedata.name;
} }
if( nodedata.name == "none" ) {
nodedata.name.clear();
} else {
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
id.Name = nodedata.name;
}
// type-based deserialization. not elegant but it'll do // type-based deserialization. not elegant but it'll do
if( nodedata.type == "dynamic" ) { if( nodedata.type == "dynamic" ) {
@@ -462,25 +463,34 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
// //
if( vehicle->mdModel != nullptr ) { if( vehicle->mdModel != nullptr ) {
for( auto const &smokesource : vehicle->mdModel->smoke_sources() ) { for( auto const &smokesource : vehicle->mdModel->smoke_sources() ) {
Particles.insert( auto& particleComponent = world.AddComponent<ECSComponent::ParticleEmitter>(entity);
smokesource.first, particleComponent.emitterLocation = smokesource.second;
vehicle, particleComponent.isActive = true;
smokesource.second );
} }
} }
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 ) + ")" ); ErrorLog( "Bad scenario: duplicate vehicle name \"" + vehicle->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );
} }
auto velocityComponent = world.AddComponent<ECSComponent::Velocity>(entity); world.AddComponent<ECSComponent::Velocity>(entity).Value = glm::dvec3(0.0);
velocityComponent.Value = glm::dvec3(0.0);
if( ( vehicle->MoverParameters->CategoryFlag == 1 ) // trains only // link ECS entity back to the live vehicle so VehicleSyncSystem can track it
&& ( ( ( vehicle->LightList( end::front ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) world.AddComponent<ECSComponent::VehicleRef>(entity).vehicle = vehicle;
|| ( ( vehicle->LightList( end::rear ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) ) ) {
// 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 ); simulation::Lights.insert( vehicle );
auto& spotlight = world.AddComponent<ECSComponent::SpotLight>(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" ) { 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 ); scene::Groups.insert( scene::Groups.handle(), instance );
simulation::Region->insert( instance ); simulation::Region->insert( instance );
ECWorld& world = Application.sceneManager.CurrentScene()->World(); {
entt::entity modelEntity = world.FindEntityByName(instance->name());
entt::entity entity = world.FindEntityByName(instance->name()); if (modelEntity != entt::null) {
auto& meshComponent = world.AddComponent<ECSComponent::MeshRenderer>(modelEntity);
if(entity != entt::null) { meshComponent.modelInstance = instance;
meshComponent.visible = true;
auto meshComponent = world.AddComponent<ECSComponent::MeshRenderer>(entity);
//meshComponent.meshHandle = instance->Model()->MeshHandle();
meshComponent.visible = true;
auto* transformComponent = world.GetComponent<ECSComponent::Transform>(entity); if (auto* t = world.GetComponent<ECSComponent::Transform>(modelEntity)) {
transformComponent->Position = {instance->location().x, instance->location().y, instance->location().z}; t->Position = { instance->location().x, instance->location().y, instance->location().z };
transformComponent->Rotation = {instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f}; t->Rotation = { instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f };
//transformComponent->Scale = {instance->scale().x, instance->scale().y, instance->scale().z}; }
// smoke sources on static models
if (instance->Model() != nullptr) {
for (auto const &smokesource : instance->Model()->smoke_sources()) {
auto& emitter = world.AddComponent<ECSComponent::ParticleEmitter>(modelEntity);
emitter.emitterLocation = smokesource.second;
emitter.isActive = true;
}
}
}
} }
scene::basic_node *hierarchy_node = instance; 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 *sound { deserialize_sound( Input, Scratchpad, nodedata ) };
auto soundComponent = world.AddComponent<ECSComponent::SoundComponent>(entity); auto& soundComponent = world.AddComponent<ECSComponent::SoundComponent>(entity);
soundComponent.sound.copy_sounds(*sound);
soundComponent.sound = *sound; soundComponent.volume = sound->gain();
soundComponent.volume = 1; soundComponent.pitch = 1.0f;
soundComponent.pitch = 1; soundComponent.range = sound->range();
soundComponent.range = sound->range(); soundComponent.loop = false;
soundComponent.isPlaying = sound->is_playing(); soundComponent.isPlaying = sound->is_playing();
soundComponent.playOnStart = false;
if( false == simulation::Sounds.insert( sound ) ) { 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 ) + ")" ); ErrorLog( "Bad scenario: duplicate sound node name \"" + sound->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );