diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index e5b0774788..cb2c863870 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2024 the openage authors. See copying.md for legal info. +// Copyright 2022-2025 the openage authors. See copying.md for legal info. #include "render_stage.h" @@ -12,6 +12,7 @@ #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" #include "renderer/stages/world/object.h" +#include "renderer/stages/world/world_shader_commands.h" #include "renderer/texture.h" #include "renderer/window.h" #include "time/clock.h" @@ -46,6 +47,30 @@ WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, log::log(INFO << "Created render stage 'World'"); } +WorldRenderStage::WorldRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const util::Path &configdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock) : + renderer{renderer}, + camera{camera}, + asset_manager{asset_manager}, + render_objects{}, + clock{clock}, + default_geometry{this->renderer->add_mesh_geometry(WorldObject::get_mesh())} { + auto size = window->get_size(); + this->initialize_render_pass_with_shader_commands(size[0], size[1], shaderdir, configdir); + this->init_uniform_ids(); + + window->add_resize_callback([this](size_t width, size_t height, double /*scale*/) { + this->resize(width, height); + }); + + log::log(INFO << "Created render stage 'World' with shader command"); +} + std::shared_ptr WorldRenderStage::get_render_pass() { return this->render_pass; } @@ -156,4 +181,37 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } +void WorldRenderStage::initialize_render_pass_with_shader_commands(size_t width, size_t height, const util::Path &shaderdir, const util::Path &config_path) { + auto vert_shader_file = (shaderdir / "demo_7_world.vert.glsl").open(); + auto vert_shader_src = renderer::resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + vert_shader_file.read()); + vert_shader_file.close(); + + auto frag_shader_file = (shaderdir / "demo_7_world.frag.glsl").open(); + log::log(INFO << "Loading shader commands config from: " << (shaderdir / "demo_7_display.frag.glsl")); + this->shader_template = std::make_shared(frag_shader_file.read()); + if (not this->shader_template->load_commands(config_path / "world_commands.config")) { + log::log(ERR << "Failed to load shader commands configuration for world stage"); + return; + } + + auto frag_shader_src = renderer::resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + this->shader_template->generate_source()); + frag_shader_file.close(); + + this->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8)); + this->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24)); + this->id_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::r32ui)); + + this->display_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src}); + this->display_shader->bind_uniform_buffer("camera", this->camera->get_uniform_buffer()); + + auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture, this->id_texture}); + this->render_pass = this->renderer->add_render_pass({}, fbo); +} + } // namespace openage::renderer::world \ No newline at end of file diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index 894ab52ff0..2c751be50d 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -1,4 +1,4 @@ -// Copyright 2022-2024 the openage authors. See copying.md for legal info. +// Copyright 2022-2025 the openage authors. See copying.md for legal info. #pragma once @@ -33,6 +33,7 @@ class AssetManager; namespace world { class RenderEntity; class WorldObject; +class ShaderCommandTemplate; /** * Renderer for drawing and displaying entities in the game world (units, buildings, etc.) @@ -60,6 +61,26 @@ class WorldRenderStage { const util::Path &shaderdir, const std::shared_ptr &asset_manager, const std::shared_ptr clock); + + /** + * Create a new render stage for the game world with shader command. + * + * @param window openage window targeted for rendering. + * @param renderer openage low-level renderer. + * @param camera Camera used for the rendered scene. + * @param shaderdir Directory containing the shader source files. + * @param configdir Directory containing the config for shader command. + * @param asset_manager Asset manager for loading resources. + * @param clock Simulation clock for timing animations. + */ + WorldRenderStage(const std::shared_ptr &window, + const std::shared_ptr &renderer, + const std::shared_ptr &camera, + const util::Path &shaderdir, + const util::Path &configdir, + const std::shared_ptr &asset_manager, + const std::shared_ptr clock); + ~WorldRenderStage() = default; /** @@ -111,6 +132,17 @@ class WorldRenderStage { */ void init_uniform_ids(); + /** + * Initialize render pass with shader commands. + * This is an alternative to initialize_render_pass() that uses configurable shader commands. + * + * @param width Width of the FBO. + * @param height Height of the FBO. + * @param shaderdir Directory containing shader files. + * @param configdir Directory containing configuration file. + */ + void initialize_render_pass_with_shader_commands(size_t width, size_t height, const util::Path &shaderdir, const util::Path &config_path); + /** * Reference to the openage renderer. */ @@ -131,6 +163,11 @@ class WorldRenderStage { */ std::shared_ptr render_pass; + /** + * Template for the world shader program. + */ + std::shared_ptr shader_template; + /** * Render entities requested by the game world. */ diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/world_shader_commands.cpp index b734d8d86a..9cc10f5f87 100644 --- a/libopenage/renderer/stages/world/world_shader_commands.cpp +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -1,4 +1,4 @@ -// Copyright 2024-2024 the openage authors. See copying.md for legal info. +// Copyright 2024-2025 the openage authors. See copying.md for legal info. #include "world_shader_commands.h" @@ -9,67 +9,158 @@ namespace openage::renderer::world { -bool WorldShaderCommands::add_command(uint8_t alpha, const std::string &code, const std::string &description) { - if (!validate_alpha(alpha)) { - log::log(ERR << "Invalid alpha value: " << int(alpha)); +ShaderCommandTemplate::ShaderCommandTemplate(const std::string &template_code) : + template_code{template_code} {} + +bool ShaderCommandTemplate::load_commands(const util::Path &config_path) { + try { + log::log(INFO << "Loading shader commands config from: " << config_path); + auto config_file = config_path.open(); + std::string line; + std::stringstream ss(config_file.read()); + + ShaderCommandConfig current_command; + // if true, we are reading the code block for the current command. + bool reading_code = false; + std::string code_block; + + while (std::getline(ss, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + // Trim whitespace from line + line = trim(line); + log::log(INFO << "Parsing line: " << line); + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') { + continue; + } + + if (reading_code) { + if (line == "}") { + reading_code = false; + current_command.code = code_block; + + // Generate and add snippet + std::string snippet = generate_snippet(current_command); + add_snippet(current_command.placeholder_id, snippet); + commands.push_back(current_command); + + // Reset for next command + code_block.clear(); + } + else { + code_block += line + "\n"; + } + continue; + } + + if (line == "[COMMAND]") { + current_command = ShaderCommandConfig{}; + continue; + } + + // Parse key-value pairs + size_t pos = line.find('='); + if (pos != std::string::npos) { + std::string key = trim(line.substr(0, pos)); + std::string value = trim(line.substr(pos + 1)); + + if (key == "placeholder") { + current_command.placeholder_id = value; + } + else if (key == "alpha") { + uint8_t alpha = static_cast(std::stoi(value)); + if (alpha % 2 == 0 && alpha >= 0 && alpha <= 254) { + current_command.alpha = alpha; + } + else { + log::log(ERR << "Invalid alpha value for command: " << alpha); + return false; + } + } + else if (key == "description") { + current_command.description = value; + } + else if (key == "code") { + if (value == "{") { + reading_code = true; + code_block.clear(); + } + } + } + } + + return true; + } + catch (const std::exception &e) { + log::log(ERR << "Failed to load shader commands: " << e.what()); return false; } - if (!validate_code(code)) { - log::log(ERR << "Invalid command code"); +} + +bool ShaderCommandTemplate::add_snippet(const std::string &placeholder_id, const std::string &snippet) { + if (snippet.empty()) { + log::log(ERR << "Empty snippet for placeholder: " << placeholder_id); return false; } - commands_map[alpha] = {alpha, code, description}; - return true; -} + if (placeholder_id.empty()) { + log::log(ERR << "Empty placeholder ID for snippet"); + return false; + } -bool WorldShaderCommands::remove_command(uint8_t alpha) { - if (!validate_alpha(alpha)) { + // Check if the placeholder exists in the template + std::string placeholder = "//@" + placeholder_id + "@"; + if (template_code.find(placeholder) == std::string::npos) { + log::log(ERR << "Placeholder not found in template: " << placeholder_id); return false; } - commands_map.erase(alpha); + + // Store the snippet + snippets[placeholder_id].push_back(snippet); return true; } -bool WorldShaderCommands::has_command(uint8_t alpha) const { - return commands_map.contains(alpha); +std::string ShaderCommandTemplate::generate_snippet(const ShaderCommandConfig &command) { + return "case " + std::to_string(command.alpha) + ":\n" + + "\t\t// " + command.description + "\n" + + "\t\t" + command.code + "\t\tbreak;\n"; } -std::string WorldShaderCommands::integrate_command(const std::string &base_shader) { - std::string final_shader = base_shader; - std::string commands_code = generate_command_code(); +std::string ShaderCommandTemplate::generate_source() const { + std::string result = template_code; - // Find the insertion point - size_t insert_point = final_shader.find(COMMAND_MARKER); - if (insert_point == std::string::npos) { - throw Error(MSG(err) << "Failed to find command insertion point in shader."); - } - - // Replace the insertion point with the generated command code - final_shader.replace(insert_point, std::strlen(COMMAND_MARKER), commands_code); + // Process each placeholder + for (const auto &[placeholder_id, snippet_list] : snippets) { + std::string combined_snippets; - return final_shader; -} + // Combine all snippets for this placeholder + for (const auto &snippet : snippet_list) { + combined_snippets += snippet; + } -std::string WorldShaderCommands::generate_command_code() const { - std::string result = ""; + // Find and replace the placeholder + std::string placeholder = "//@" + placeholder_id + "@"; + size_t pos = result.find(placeholder); + if (pos == std::string::npos) { + throw Error(MSG(err) << "Placeholder disappeared from template: " << placeholder_id); + } - for (const auto &[alpha, command] : commands_map) { - result += " case " + std::to_string(alpha) + ":\n"; - result += " // " + command.description + "\n"; - result += " " + command.code + "\n"; - result += " break;\n\n"; + // Replace placeholder with combined snippets + result.replace(pos, placeholder.length(), combined_snippets); } return result; } -bool WorldShaderCommands::validate_alpha(uint8_t alpha) const { - return alpha % 2 == 0 && alpha >= 0 && alpha <= 254; -} - -bool WorldShaderCommands::validate_code(const std::string &code) const { - return !code.empty(); +std::string ShaderCommandTemplate::trim(const std::string &str) const { + size_t first = str.find_first_not_of(" \t"); + if (first == std::string::npos) { + return ""; + } + size_t last = str.find_last_not_of(" \t"); + return str.substr(first, (last - first + 1)); } - } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/world_shader_commands.h b/libopenage/renderer/stages/world/world_shader_commands.h index d025735db2..9f6a158a25 100644 --- a/libopenage/renderer/stages/world/world_shader_commands.h +++ b/libopenage/renderer/stages/world/world_shader_commands.h @@ -1,11 +1,14 @@ -// Copyright 2024-2024 the openage authors. See copying.md for legal info. +// Copyright 2024-2025 the openage authors. See copying.md for legal info. #pragma once #include #include +#include #include +#include "util/path.h" + namespace openage { namespace renderer { namespace world { @@ -15,94 +18,69 @@ namespace world { * Commands are identified by their alpha values and contain GLSL code snippets * that define custom rendering behavior. */ -struct ShaderCommand { - // Command identifier ((must be even, range 0-254)) +struct ShaderCommandConfig { + /// ID of the placeholder where this snippet should be inserted + std::string placeholder_id; + /// Command identifier ((must be even, range 0-254)) uint8_t alpha; - // GLSL code snippet that defines the command's behavior + /// GLSL code snippet that defines the command's behavior std::string code; - // Documentation (optional) + /// Documentation (optional) std::string description; }; /** - * Manages shader commands for the world fragment shader. - * Provides functionality to add, remove, and integrate commands into the base shader. - * Commands are inserted at a predefined marker in the shader code. + * Manages shader templates and their code snippets. + * Allows loading configurable shader commands and generating + * complete shader source code. */ -class WorldShaderCommands { +class ShaderCommandTemplate { public: - // Marker in shader code where commands will be inserted - static constexpr const char *COMMAND_MARKER = "//@INSERT_COMMANDS@"; - /** - * Add a new shader command. - * - * @param alpha Command identifier (must be even, range 0-254) - * @param code GLSL code snippet defining the command's behavior - * @param description Human-readable description of the command's purpose + * Create a shader template from source code of shader. * - * @return true if command was added successfully, false if validation failed + * @param template_code Source code containing placeholders. */ - bool add_command(uint8_t alpha, const std::string &code, const std::string &description = ""); + explicit ShaderCommandTemplate(const std::string &template_code); /** - * Remove a command. + * Load commands from a configuration file. * - * @param alpha Command identifier (even values 0-254) + * @param config_path Path to the command configuration file. + * @return true if commands were loaded successfully. */ - bool remove_command(uint8_t alpha); + bool load_commands(const util::Path &config_path); /** - * Check if a command is registered. + * Add a single code snippet to the template. * - * @param alpha Command identifier to check - * - * @return true if command is registered + * @param placeholder_id Where to insert the snippet. + * @param snippet Code to insert. + * @return true if snippet was added successfully. */ - bool has_command(uint8_t alpha) const; + bool add_snippet(const std::string &placeholder_id, const std::string &snippet); /** - * Integrate registered commands into the base shader code. - * - * @param base_shader Original shader code containing the command marker - * - * @return Complete shader code with commands integrated at the marker position + * Generate final shader source code with all snippets inserted. * - * @throws Error if command marker is not found in the base shader + * @return Complete shader code. + * @throws Error if any required placeholders are missing snippets. */ - std::string integrate_command(const std::string &base_shader); + std::string generate_source() const; private: - /** - * Generate GLSL code for all registered commands. - * - * @return String containing case statements for each command - */ - std::string generate_command_code() const; + // Generate a single code snippet for a command. + std::string generate_snippet(const ShaderCommandConfig &command); + // Helper function to trim whitespace from a string + std::string trim(const std::string &str) const; - /** - * Validate a command identifier. - * - * @param alpha Command identifier to validate - * - * @return true if alpha is even and within valid range (0-254) - */ - bool validate_alpha(uint8_t alpha) const; - - /** - * Validate command GLSL code. - * - * @param code GLSL code snippet to validate - * - * @return true if code is not empty (additional validation could be added) - */ - - bool validate_code(const std::string &code) const; - - // Map of command identifiers to their respective commands - std::map commands_map; + // Original template code with placeholders + std::string template_code; + // Mapping of placeholder IDs to their code snippets + std::map> snippets; + // Loaded command configurations + std::vector commands; }; - } // namespace world } // namespace renderer } // namespace openage