From f94d34e3680162df32640832ca00e72056e5be68 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 29 Dec 2024 15:25:11 -0500 Subject: [PATCH 1/7] add world shader commands implementation --- .../renderer/stages/world/CMakeLists.txt | 1 + .../stages/world/world_shader_commands.cpp | 73 ++++++++++++ .../stages/world/world_shader_commands.h | 108 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 libopenage/renderer/stages/world/world_shader_commands.cpp create mode 100644 libopenage/renderer/stages/world/world_shader_commands.h diff --git a/libopenage/renderer/stages/world/CMakeLists.txt b/libopenage/renderer/stages/world/CMakeLists.txt index 811fd929ed..d86d7aa44c 100644 --- a/libopenage/renderer/stages/world/CMakeLists.txt +++ b/libopenage/renderer/stages/world/CMakeLists.txt @@ -2,4 +2,5 @@ add_sources(libopenage object.cpp render_entity.cpp render_stage.cpp + world_shader_commands.cpp ) diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/world_shader_commands.cpp new file mode 100644 index 0000000000..e7b31f87c1 --- /dev/null +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -0,0 +1,73 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "world_shader_commands.h" + +#include "error/error.h" +#include "log/log.h" + +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)); + return false; + } + if (!validate_code(code)) { + log::log(ERR << "Invalid command code"); + return false; + } + + commands_map[alpha] = {alpha, code, description}; + return true; +} + +bool WorldShaderCommands::remove_command(uint8_t alpha) { + if (!validate_alpha(alpha)) { + return false; + } + commands_map.erase(alpha); + return true; +} + +bool WorldShaderCommands::has_command(uint8_t alpha) const { + return commands_map.contains(alpha); +} + +std::string WorldShaderCommands::integrate_command(const std::string &base_shader) { + std::string final_shader = base_shader; + std::string commands_code = generate_command_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, strlen(COMMAND_MARKER), commands_code); + + return final_shader; +} + +std::string WorldShaderCommands::generate_command_code() const { + std::string result = ""; + + 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"; + } + + 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(); +} + +} // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/world_shader_commands.h b/libopenage/renderer/stages/world/world_shader_commands.h new file mode 100644 index 0000000000..d025735db2 --- /dev/null +++ b/libopenage/renderer/stages/world/world_shader_commands.h @@ -0,0 +1,108 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +namespace openage { +namespace renderer { +namespace world { + +/** + * Represents a single shader command that can be used in the world fragment shader. + * 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)) + uint8_t alpha; + // GLSL code snippet that defines the command's behavior + std::string code; + // 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. + */ +class WorldShaderCommands { +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 + * + * @return true if command was added successfully, false if validation failed + */ + bool add_command(uint8_t alpha, const std::string &code, const std::string &description = ""); + + /** + * Remove a command. + * + * @param alpha Command identifier (even values 0-254) + */ + bool remove_command(uint8_t alpha); + + /** + * Check if a command is registered. + * + * @param alpha Command identifier to check + * + * @return true if command is registered + */ + bool has_command(uint8_t alpha) const; + + /** + * 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 + * + * @throws Error if command marker is not found in the base shader + */ + std::string integrate_command(const std::string &base_shader); + +private: + /** + * Generate GLSL code for all registered commands. + * + * @return String containing case statements for each command + */ + std::string generate_command_code() 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; +}; + +} // namespace world +} // namespace renderer +} // namespace openage From 235fcf340a17b850955344d349be7745e6e50325 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 29 Dec 2024 15:26:41 -0500 Subject: [PATCH 2/7] use world shader commands in render_stage & update world2d.frag --- assets/shaders/world2d.frag.glsl | 10 +------ .../renderer/stages/world/render_stage.cpp | 28 +++++++++++++++++-- .../renderer/stages/world/render_stage.h | 13 +++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/assets/shaders/world2d.frag.glsl b/assets/shaders/world2d.frag.glsl index 98d323e0f6..f9de0354ba 100644 --- a/assets/shaders/world2d.frag.glsl +++ b/assets/shaders/world2d.frag.glsl @@ -26,15 +26,7 @@ void main() { // do not save the ID return; - case 254: - col = vec4(1.0f, 0.0f, 0.0f, 1.0f); - break; - case 252: - col = vec4(0.0f, 1.0f, 0.0f, 1.0f); - break; - case 250: - col = vec4(0.0f, 0.0f, 1.0f, 1.0f); - break; + //@INSERT_COMMANDS@ default: col = tex_val; break; diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d8d93279af..3962daa170 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -127,12 +127,18 @@ void WorldRenderStage::initialize_render_pass(size_t width, vert_shader_file.read()); vert_shader_file.close(); + // Initialize shader command system before loading fragment shader + this->shader_commands = std::make_unique(); + this->init_shader_commands(); + auto frag_shader_file = (shaderdir / "world2d.frag.glsl").open(); + auto base_shader = frag_shader_file.read(); + frag_shader_file.close(); + auto frag_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::fragment, - frag_shader_file.read()); - frag_shader_file.close(); + this->shader_commands->integrate_command(base_shader)); 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)); @@ -156,4 +162,22 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } +void WorldRenderStage::init_shader_commands() { + // Register default shader commands + this->shader_commands->add_command( + 254, + "col = vec4(1.0f, 0.0f, 0.0f, 1.0f);", + "Red tint command"); + this->shader_commands->add_command( + 252, + "col = vec4(0.0f, 1.0f, 0.0f, 1.0f);", + "Green tint command"); + this->shader_commands->add_command( + 250, + "col = vec4(0.0f, 0.0f, 1.0f, 1.0f);", + "Blue tint command"); + + // Additional commands can be added here +} + } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index f1256f3b57..f048a1cbee 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -7,6 +7,7 @@ #include #include "util/path.h" +#include "world_shader_commands.h" namespace openage { @@ -111,6 +112,12 @@ class WorldRenderStage { */ void init_uniform_ids(); + /** + * Initialize the shader command system and register default commands. + * This must be called before initializing the shader program. + */ + void init_shader_commands(); + /** * Reference to the openage renderer. */ @@ -174,6 +181,12 @@ class WorldRenderStage { * Mutex for protecting threaded access. */ std::shared_mutex mutex; + + /** + * Shader command system for the world fragment shader. + * Manages custom rendering behaviors through alpha channel commands. + */ + std::unique_ptr shader_commands; }; } // namespace world } // namespace renderer From 3dc759fa685f5a5935e902ac4278a34706878262 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 29 Dec 2024 16:03:08 -0500 Subject: [PATCH 3/7] fix a cstring error --- libopenage/renderer/stages/world/world_shader_commands.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libopenage/renderer/stages/world/world_shader_commands.cpp b/libopenage/renderer/stages/world/world_shader_commands.cpp index e7b31f87c1..b734d8d86a 100644 --- a/libopenage/renderer/stages/world/world_shader_commands.cpp +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -2,6 +2,8 @@ #include "world_shader_commands.h" +#include + #include "error/error.h" #include "log/log.h" @@ -44,7 +46,7 @@ std::string WorldShaderCommands::integrate_command(const std::string &base_shade } // Replace the insertion point with the generated command code - final_shader.replace(insert_point, strlen(COMMAND_MARKER), commands_code); + final_shader.replace(insert_point, std::strlen(COMMAND_MARKER), commands_code); return final_shader; } From 64620ae143b735e656b28e7466870c26952dd0cb Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Sun, 19 Jan 2025 20:08:28 -0500 Subject: [PATCH 4/7] reset world2d shader and render_entity --- assets/shaders/world2d.frag.glsl | 12 ++++++-- .../renderer/stages/world/render_stage.cpp | 30 ++----------------- .../renderer/stages/world/render_stage.h | 15 +--------- 3 files changed, 14 insertions(+), 43 deletions(-) diff --git a/assets/shaders/world2d.frag.glsl b/assets/shaders/world2d.frag.glsl index f9de0354ba..143d3f0980 100644 --- a/assets/shaders/world2d.frag.glsl +++ b/assets/shaders/world2d.frag.glsl @@ -26,10 +26,18 @@ void main() { // do not save the ID return; - //@INSERT_COMMANDS@ + case 254: + col = vec4(1.0f, 0.0f, 0.0f, 1.0f); + break; + case 252: + col = vec4(0.0f, 1.0f, 0.0f, 1.0f); + break; + case 250: + col = vec4(0.0f, 0.0f, 1.0f, 1.0f); + break; default: col = tex_val; break; } id = u_id; -} +} \ No newline at end of file diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index 3962daa170..e5b0774788 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -127,18 +127,12 @@ void WorldRenderStage::initialize_render_pass(size_t width, vert_shader_file.read()); vert_shader_file.close(); - // Initialize shader command system before loading fragment shader - this->shader_commands = std::make_unique(); - this->init_shader_commands(); - auto frag_shader_file = (shaderdir / "world2d.frag.glsl").open(); - auto base_shader = frag_shader_file.read(); - frag_shader_file.close(); - auto frag_shader_src = renderer::resources::ShaderSource( resources::shader_lang_t::glsl, resources::shader_stage_t::fragment, - this->shader_commands->integrate_command(base_shader)); + frag_shader_file.read()); + 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)); @@ -162,22 +156,4 @@ void WorldRenderStage::init_uniform_ids() { WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); } -void WorldRenderStage::init_shader_commands() { - // Register default shader commands - this->shader_commands->add_command( - 254, - "col = vec4(1.0f, 0.0f, 0.0f, 1.0f);", - "Red tint command"); - this->shader_commands->add_command( - 252, - "col = vec4(0.0f, 1.0f, 0.0f, 1.0f);", - "Green tint command"); - this->shader_commands->add_command( - 250, - "col = vec4(0.0f, 0.0f, 1.0f, 1.0f);", - "Blue tint command"); - - // Additional commands can be added here -} - -} // namespace openage::renderer::world +} // 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 f048a1cbee..894ab52ff0 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -7,7 +7,6 @@ #include #include "util/path.h" -#include "world_shader_commands.h" namespace openage { @@ -112,12 +111,6 @@ class WorldRenderStage { */ void init_uniform_ids(); - /** - * Initialize the shader command system and register default commands. - * This must be called before initializing the shader program. - */ - void init_shader_commands(); - /** * Reference to the openage renderer. */ @@ -181,13 +174,7 @@ class WorldRenderStage { * Mutex for protecting threaded access. */ std::shared_mutex mutex; - - /** - * Shader command system for the world fragment shader. - * Manages custom rendering behaviors through alpha channel commands. - */ - std::unique_ptr shader_commands; }; } // namespace world } // namespace renderer -} // namespace openage +} // namespace openage \ No newline at end of file From 8cda18bfb0a15eb585487cb7d4798b45c09d806c Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 23 Jan 2025 17:08:08 -0500 Subject: [PATCH 5/7] implement more controllable shader command template --- .../renderer/stages/world/render_stage.cpp | 60 +++++- .../renderer/stages/world/render_stage.h | 39 +++- .../stages/world/world_shader_commands.cpp | 173 +++++++++++++----- .../stages/world/world_shader_commands.h | 102 ++++------- 4 files changed, 269 insertions(+), 105 deletions(-) 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 From 84abb5d1441f8b971034b5131f2afaed56d848c9 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 23 Jan 2025 17:08:55 -0500 Subject: [PATCH 6/7] add demo7 to test shader command --- assets/test/shaders/demo_7_world.frag.glsl | 34 +++++++ assets/test/shaders/demo_7_world.vert.glsl | 101 ++++++++++++++++++++ assets/test/shaders/world_commands.config | 23 +++++ libopenage/renderer/demo/CMakeLists.txt | 1 + libopenage/renderer/demo/demo_7.cpp | 106 +++++++++++++++++++++ libopenage/renderer/demo/demo_7.h | 22 +++++ libopenage/renderer/demo/tests.cpp | 7 +- 7 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 assets/test/shaders/demo_7_world.frag.glsl create mode 100644 assets/test/shaders/demo_7_world.vert.glsl create mode 100644 assets/test/shaders/world_commands.config create mode 100644 libopenage/renderer/demo/demo_7.cpp create mode 100644 libopenage/renderer/demo/demo_7.h diff --git a/assets/test/shaders/demo_7_world.frag.glsl b/assets/test/shaders/demo_7_world.frag.glsl new file mode 100644 index 0000000000..d6ffd22029 --- /dev/null +++ b/assets/test/shaders/demo_7_world.frag.glsl @@ -0,0 +1,34 @@ +#version 330 + +in vec2 vert_uv; + +layout(location = 0) out vec4 col; +layout(location = 1) out uint id; + +uniform sampler2D tex; +uniform uint u_id; + +// position (top left corner) and size: (x, y, width, height) +uniform vec4 tile_params; + +vec2 uv = vec2( + vert_uv.x * tile_params.z + tile_params.x, + vert_uv.y *tile_params.w + tile_params.y); + +void main() { + vec4 tex_val = texture(tex, uv); + int alpha = int(round(tex_val.a * 255)); + switch (alpha) { + case 0: + col = tex_val; + discard; + + // do not save the ID + return; + //@COMMAND_SWITCH@ + default: + col = tex_val; + break; + } + id = u_id; +} \ No newline at end of file diff --git a/assets/test/shaders/demo_7_world.vert.glsl b/assets/test/shaders/demo_7_world.vert.glsl new file mode 100644 index 0000000000..7987d40ec3 --- /dev/null +++ b/assets/test/shaders/demo_7_world.vert.glsl @@ -0,0 +1,101 @@ +#version 330 + +layout(location=0) in vec2 v_position; +layout(location=1) in vec2 uv; + +out vec2 vert_uv; + +// camera parameters for transforming the object position +// and scaling the subtex to the correct size +layout (std140) uniform camera { + // view matrix (world to view space) + mat4 view; + // projection matrix (view to clip space) + mat4 proj; + // inverse zoom factor (1.0 / zoom) + // high zoom = upscale subtex + // low zoom = downscale subtex + float inv_zoom; + // inverse viewport size (1.0 / viewport size) + vec2 inv_viewport_size; +}; + +// can be used to move the object position in world space _before_ +// it's transformed to clip space +// this is usually unnecessary because we want to draw the +// subtex where the object is, so this can be set to the identity matrix +uniform mat4 model; + +// position of the object in world space +uniform vec3 obj_world_position; + +// flip the subtexture horizontally/vertically +uniform bool flip_x; +uniform bool flip_y; + +// parameters for scaling and moving the subtex +// to the correct position in clip space + +// animation scalefactor +// scales the vertex positions so that they +// match the subtex dimensions +// +// high animation scale = downscale subtex +// low animation scale = upscale subtex +uniform float scale; + +// size of the subtex (in pixels) +uniform vec2 subtex_size; + +// offset of the subtex anchor point +// from the subtex center (in pixels) +// used to move the subtex so that the anchor point +// is at the object position +uniform vec2 anchor_offset; + +void main() { + // translate the position of the object from world space to clip space + // this is the position where we want to draw the subtex in 2D + vec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0); + + // subtex has to be scaled to account for the zoom factor + // and the animation scale factor. essentially this is (animation scale / zoom). + float zoom_scale = scale * inv_zoom; + + // Scale the subtex vertices + // we have to account for the viewport size to get the correct dimensions + // and then scale the subtex to the zoom factor to get the correct size + vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size; + + // Scale the anchor offset with the same method as above + // to get the correct anchor position in the viewport + vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size; + + // if the subtex is flipped, we also need to flip the anchor offset + // essentially, we invert the coordinates for the flipped axis + float anchor_x = float(flip_x) * -1.0 * anchor_scale.x + float(!flip_x) * anchor_scale.x; + float anchor_y = float(flip_y) * -1.0 * anchor_scale.y + float(!flip_y) * anchor_scale.y; + + // offset the clip position by the offset of the subtex anchor + // imagine this as pinning the subtex to the object position at the subtex anchor point + obj_clip_pos += vec4(anchor_x, anchor_y, 0.0, 0.0); + + // create a move matrix for positioning the vertices + // uses the vert scale and the transformed object position in clip space + mat4 move = mat4(vert_scale.x, 0.0, 0.0, 0.0, + 0.0, vert_scale.y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0); + + // calculate the final vertex position + gl_Position = move * vec4(v_position, 0.0, 1.0); + + // if the subtex is flipped, we also need to flip the uv tex coordinates + // essentially, we invert the coordinates for the flipped axis + + // !flip_x is default because OpenGL uses bottom-left as its origin + float uv_x = float(!flip_x) * uv.x + float(flip_x) * (1.0 - uv.x); + float uv_y = float(flip_y) * uv.y + float(!flip_y) * (1.0 - uv.y); + + vert_uv = vec2(uv_x, uv_y); +} diff --git a/assets/test/shaders/world_commands.config b/assets/test/shaders/world_commands.config new file mode 100644 index 0000000000..16b6a9d399 --- /dev/null +++ b/assets/test/shaders/world_commands.config @@ -0,0 +1,23 @@ +[COMMAND] +placeholder=COMMAND_SWITCH +alpha=254 +description=Red tint +code={ +col = vec4(1.0, 0.0, 0.0, 1.0) * tex_val; +} + +[COMMAND] +placeholder=COMMAND_SWITCH +alpha=252 +description=Green tint +code={ +col = vec4(0.0, 1.0, 0.0, 1.0) * tex_val; +} + +[COMMAND] +placeholder=COMMAND_SWITCH +alpha=250 +description=Blue tint +code={ +col = vec4(0.0, 0.0, 1.0, 1.0) * tex_val; +} \ No newline at end of file diff --git a/libopenage/renderer/demo/CMakeLists.txt b/libopenage/renderer/demo/CMakeLists.txt index fb93e5279b..7567731afd 100644 --- a/libopenage/renderer/demo/CMakeLists.txt +++ b/libopenage/renderer/demo/CMakeLists.txt @@ -6,6 +6,7 @@ add_sources(libopenage demo_4.cpp demo_5.cpp demo_6.cpp + demo_7.cpp stresstest_0.cpp stresstest_1.cpp tests.cpp diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp new file mode 100644 index 0000000000..a3a21bf94d --- /dev/null +++ b/libopenage/renderer/demo/demo_7.cpp @@ -0,0 +1,106 @@ +// demo_shader_commands.h +#pragma once + +#include "util/path.h" + +#include +#include + +#include "coord/tile.h" +#include "renderer/camera/camera.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_factory.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" +#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/shader_source.h" +#include "renderer/stages/camera/manager.h" +#include "renderer/stages/screen/render_stage.h" +#include "renderer/stages/skybox/render_stage.h" +#include "renderer/stages/terrain/render_entity.h" +#include "renderer/stages/terrain/render_stage.h" +#include "renderer/stages/world/render_entity.h" +#include "renderer/stages/world/render_stage.h" +#include "renderer/uniform_buffer.h" +#include "time/clock.h" + +namespace openage::renderer::tests { + +void renderer_demo_7(const util::Path &path) { + // Basic setup + auto qtapp = std::make_shared(); + window_settings settings; + settings.width = 800; + settings.height = 600; + settings.debug = true; + + auto window = std::make_shared("Shader Commands Demo", settings); + auto renderer = window->make_renderer(); + auto camera = std::make_shared(renderer, window->get_size()); + auto clock = std::make_shared(); + auto asset_manager = std::make_shared( + renderer, + path["assets"]["test"]); + auto cam_manager = std::make_shared(camera); + + auto shaderdir = path / "assets" / "test" / "shaders"; + + std::vector> + render_passes{}; + + // Initialize world renderer with shader commands + auto world_renderer = std::make_shared( + window, + renderer, + camera, + shaderdir, + shaderdir, // Temporarily, Shader commands config has the same path with shaders for this demo + asset_manager, + clock); + + render_passes.push_back(world_renderer->get_render_pass()); + + auto screen_renderer = std::make_shared( + window, + renderer, + path["assets"]["shaders"]); + std::vector> targets{}; + for (auto &pass : render_passes) { + targets.push_back(pass->get_target()); + } + screen_renderer->set_render_targets(targets); + + render_passes.push_back(screen_renderer->get_render_pass()); + + auto render_factory = std::make_shared(nullptr, world_renderer); + + auto entity1 = render_factory->add_world_render_entity(); + entity1->update(0, coord::phys3(0.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + + auto entity2 = render_factory->add_world_render_entity(); + entity2->update(1, coord::phys3(3.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + + auto entity3 = render_factory->add_world_render_entity(); + entity3->update(2, coord::phys3(-3.0f, 0.0f, 0.0f), "./textures/test_gaben.sprite"); + + // Main loop + while (not window->should_close()) { + qtapp->process_events(); + + // Update camera matrices + cam_manager->update(); + + world_renderer->update(); + + for (auto &pass : render_passes) { + renderer->render(pass); + } + + renderer->check_error(); + + window->update(); + } +} + +} // namespace openage::renderer::tests \ No newline at end of file diff --git a/libopenage/renderer/demo/demo_7.h b/libopenage/renderer/demo/demo_7.h new file mode 100644 index 0000000000..45c192523d --- /dev/null +++ b/libopenage/renderer/demo/demo_7.h @@ -0,0 +1,22 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + +namespace openage::renderer::tests { + +/** + * Show off the render stages in the level 2 renderer and the camera + * system. + * - Window creation + * - Creating a camera + * - Initializing the level 2 render stages: skybox, terrain, world, screen + * - Adding renderables to the render stages via the render factory + * - Moving camera with mouse/keyboard callbacks + * + * @param path Path to the project rootdir. + */ +void renderer_demo_7(const util::Path &path); + +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/tests.cpp b/libopenage/renderer/demo/tests.cpp index d3fb0e3c21..bab82af2e3 100644 --- a/libopenage/renderer/demo/tests.cpp +++ b/libopenage/renderer/demo/tests.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #include "tests.h" @@ -12,6 +12,7 @@ #include "renderer/demo/demo_4.h" #include "renderer/demo/demo_5.h" #include "renderer/demo/demo_6.h" +#include "renderer/demo/demo_7.h" #include "renderer/demo/stresstest_0.h" #include "renderer/demo/stresstest_1.h" @@ -47,6 +48,10 @@ void renderer_demo(int demo_id, const util::Path &path) { renderer_demo_6(path); break; + case 7: + renderer_demo_7(path); + break; + default: log::log(MSG(err) << "Unknown renderer demo requested: " << demo_id << "."); break; From dfc8cd16ada8ac4bc25529f965fae2a30e943d37 Mon Sep 17 00:00:00 2001 From: Alex Zhuohao He Date: Thu, 23 Jan 2025 17:20:42 -0500 Subject: [PATCH 7/7] update to pass sanity_check --- libopenage/renderer/demo/demo_7.cpp | 7 ++++--- libopenage/renderer/stages/world/render_stage.cpp | 2 +- libopenage/renderer/stages/world/render_stage.h | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libopenage/renderer/demo/demo_7.cpp b/libopenage/renderer/demo/demo_7.cpp index a3a21bf94d..470ded48f1 100644 --- a/libopenage/renderer/demo/demo_7.cpp +++ b/libopenage/renderer/demo/demo_7.cpp @@ -1,5 +1,6 @@ -// demo_shader_commands.h -#pragma once +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "demo_7.h" #include "util/path.h" @@ -103,4 +104,4 @@ void renderer_demo_7(const util::Path &path) { } } -} // namespace openage::renderer::tests \ No newline at end of file +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index cb2c863870..7514f2ca7e 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -214,4 +214,4 @@ void WorldRenderStage::initialize_render_pass_with_shader_commands(size_t width, this->render_pass = this->renderer->add_render_pass({}, fbo); } -} // namespace openage::renderer::world \ No newline at end of file +} // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index 2c751be50d..9fc4cdbf06 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -214,4 +214,4 @@ class WorldRenderStage { }; } // namespace world } // namespace renderer -} // namespace openage \ No newline at end of file +} // namespace openage