diff --git a/assets/shaders/world2d.frag.glsl b/assets/shaders/world2d.frag.glsl index 98d323e0f6..143d3f0980 100644 --- a/assets/shaders/world2d.frag.glsl +++ b/assets/shaders/world2d.frag.glsl @@ -40,4 +40,4 @@ void main() { break; } id = u_id; -} +} \ No newline at end of file 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..470ded48f1 --- /dev/null +++ b/libopenage/renderer/demo/demo_7.cpp @@ -0,0 +1,107 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "demo_7.h" + +#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 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; 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/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d8d93279af..7514f2ca7e 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 diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index f1256f3b57..9fc4cdbf06 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 new file mode 100644 index 0000000000..9cc10f5f87 --- /dev/null +++ b/libopenage/renderer/stages/world/world_shader_commands.cpp @@ -0,0 +1,166 @@ +// Copyright 2024-2025 the openage authors. See copying.md for legal info. + +#include "world_shader_commands.h" + +#include + +#include "error/error.h" +#include "log/log.h" + +namespace openage::renderer::world { + +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; + } +} + +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; + } + + if (placeholder_id.empty()) { + log::log(ERR << "Empty placeholder ID for snippet"); + return false; + } + + // 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; + } + + // Store the snippet + snippets[placeholder_id].push_back(snippet); + return true; +} + +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 ShaderCommandTemplate::generate_source() const { + std::string result = template_code; + + // Process each placeholder + for (const auto &[placeholder_id, snippet_list] : snippets) { + std::string combined_snippets; + + // Combine all snippets for this placeholder + for (const auto &snippet : snippet_list) { + combined_snippets += snippet; + } + + // 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); + } + + // Replace placeholder with combined snippets + result.replace(pos, placeholder.length(), combined_snippets); + } + + return result; +} + +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 new file mode 100644 index 0000000000..9f6a158a25 --- /dev/null +++ b/libopenage/renderer/stages/world/world_shader_commands.h @@ -0,0 +1,86 @@ +// 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 { + +/** + * 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 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 + std::string code; + /// Documentation (optional) + std::string description; +}; + +/** + * Manages shader templates and their code snippets. + * Allows loading configurable shader commands and generating + * complete shader source code. + */ +class ShaderCommandTemplate { +public: + /** + * Create a shader template from source code of shader. + * + * @param template_code Source code containing placeholders. + */ + explicit ShaderCommandTemplate(const std::string &template_code); + + /** + * Load commands from a configuration file. + * + * @param config_path Path to the command configuration file. + * @return true if commands were loaded successfully. + */ + bool load_commands(const util::Path &config_path); + + /** + * Add a single code snippet to the template. + * + * @param placeholder_id Where to insert the snippet. + * @param snippet Code to insert. + * @return true if snippet was added successfully. + */ + bool add_snippet(const std::string &placeholder_id, const std::string &snippet); + + /** + * Generate final shader source code with all snippets inserted. + * + * @return Complete shader code. + * @throws Error if any required placeholders are missing snippets. + */ + std::string generate_source() const; + +private: + // 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; + + // 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