diff --git a/assets/test/shaders/demo_7_shader_command.frag.glsl b/assets/test/shaders/demo_7_shader_command.frag.glsl new file mode 100644 index 0000000000..76e50ffd78 --- /dev/null +++ b/assets/test/shaders/demo_7_shader_command.frag.glsl @@ -0,0 +1,12 @@ +#version 330 + +in vec2 tex_coord; +out vec4 frag_color; + +uniform float time; + +void main() { + // PLACEHOLDER: color + // PLACEHOLDER: alpha + frag_color = vec4(r, g, b, alpha); +} diff --git a/assets/test/shaders/demo_7_shader_command.vert.glsl b/assets/test/shaders/demo_7_shader_command.vert.glsl new file mode 100644 index 0000000000..caefe64535 --- /dev/null +++ b/assets/test/shaders/demo_7_shader_command.vert.glsl @@ -0,0 +1,9 @@ +#version 330 + +in vec2 position; +out vec2 tex_coord; + +void main() { + tex_coord = position.xy * 0.5 + 0.5; + gl_Position = vec4(position.xy, 0.0, 1.0); +} diff --git a/assets/test/shaders/demo_7_snippets/alpha.snippet b/assets/test/shaders/demo_7_snippets/alpha.snippet new file mode 100644 index 0000000000..603c275311 --- /dev/null +++ b/assets/test/shaders/demo_7_snippets/alpha.snippet @@ -0,0 +1 @@ +float alpha = (sin(time) + 1.0) * 0.5; diff --git a/assets/test/shaders/demo_7_snippets/color.snippet b/assets/test/shaders/demo_7_snippets/color.snippet new file mode 100644 index 0000000000..4c0204e773 --- /dev/null +++ b/assets/test/shaders/demo_7_snippets/color.snippet @@ -0,0 +1,3 @@ +float r = 0.5 + 0.5 * sin(time); +float g = 0.5 + 0.5 * sin(time + 2.0); +float b = 0.5 + 0.5 * sin(time + 4.0); diff --git a/doc/code/renderer/demos.md b/doc/code/renderer/demos.md index 3bb7b266f3..3be974d628 100644 --- a/doc/code/renderer/demos.md +++ b/doc/code/renderer/demos.md @@ -144,6 +144,19 @@ This demo shows how to use [frustum culling](level2.md#frustum-culling) in the r ![Demo 6](/doc/code/renderer/images/demo_6.png) +### Demo 6 + +This demo shows how to use [shader templating](level1.md#shader-templates) in the renderer. + +```bash +./bin/run test --demo renderer.tests.renderer_demo 6 +``` + +**Result:** + +![Demo 7](/doc/code/renderer/images/demo_7.mp4) + + ## Stresstests ### Stresstest 0 diff --git a/doc/code/renderer/images/demo_7.mp4 b/doc/code/renderer/images/demo_7.mp4 new file mode 100644 index 0000000000..fa26db0f86 Binary files /dev/null and b/doc/code/renderer/images/demo_7.mp4 differ diff --git a/doc/code/renderer/level1.md b/doc/code/renderer/level1.md index 659c51036c..b8dbc8fb30 100644 --- a/doc/code/renderer/level1.md +++ b/doc/code/renderer/level1.md @@ -17,6 +17,7 @@ Low-level renderer for communicating with the OpenGL and Vulkan APIs. 3. [Defining Layers in a Render Pass](#defining-layers-in-a-render-pass) 4. [Complex Geometry](#complex-geometry) 5. [Uniform Buffers](#uniform-buffers) + 6. [Shader Templates](#shader-templates) 5. [Thread-safety](#thread-safety) @@ -542,5 +543,80 @@ resources::UniformBufferInfo ubo_info{ std::shared_ptr buffer = renderer->add_uniform_buffer(ubo_info); ``` +### Shader Templates + +Shader templates allow the definition of a shader source code with placeholders that can be replaced at +load- or run-time. Templates are help if only specific parts of the shader code are supposed to +be configurable. This may be used, for example, to alter certain code path of the built-in shaders +without recompiling the engine. + +An example shader template looks like this: + +```glsl +#version 330 + +in vec2 tex_coord; +out vec4 frag_color; + +// PLACEHOLDER: uniform_color + +void main() { + // PLACEHOLDER: alpha + frag_color = vec4(col.xyz, alpha); +} +``` + +Placeholders are inserted as comments into the GLSL source. Every placeholder has an ID for +referencing. For these IDs, *snippets* can be defined to insert code in place of the placeholder +comment. Placeholders can be placed at any position in the template, so they can be used to insert +other statements than the control code, including unforms, input/output variables, functions and more. + +The snippets for the above example may look like this: + +`uniform_color.snippet` + +```glsl +uniform vec4 col; +``` + +`alpha.snippet` + +```glsl +float alpha = 0.5; +``` + +In the renderer, shader templates can be created using the `renderer::resources::ShaderTemplate` class. + +```cpp +util::Path template_path = shaderdir / "example_template.frag.glsl"; +resources::ShaderTemplate template(template_path); +``` + +After loading the template, snippets for the placeholders can be added. These are either loaded +as single files or from a directory. + +```cpp +util::Path snippets_path = shaderdir / "example_snippets"; +template.load_snippets(snippets_path); +``` + +or + +```cpp +util::Path snippets_path = shaderdir / "example_snippets"; +template.add_snippet(snippets_path / "uniform_color.snippet"); +template.add_snippet(snippets_path / "alpha.snippet"); +``` + +If snippets have been loaded for all placeholder IDs, the shader template can generate the final +shader source code as `renderer::resources::ShaderSource`. This can then be used to create the actual +`renderer::ShaderProgram` uploaded to the GPU. + +```cpp +resources::ShaderSource source = template.generate_source(); +ShaderProgram prog = = renderer->add_shader({source}); +``` + + ## Thread-safety This level might or might not be threadsafe depending on the concrete backend. The OpenGL version is, in typical GL fashion, so not-threadsafe it's almost anti-threadsafe. All code must be executed sequentially on a dedicated window thread, the same one on which the window and renderer were initially created. The plan for the Vulkan version is to make it at least independent of thread-local storage and hopefully completely threadsafe. 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..8c804a52d9 --- /dev/null +++ b/libopenage/renderer/demo/demo_7.cpp @@ -0,0 +1,83 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "demo_7.h" + +#include "util/path.h" + +#include "renderer/demo/util.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/render_target.h" +#include "renderer/resources/mesh_data.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/shader_template.h" +#include "renderer/shader_program.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; + + opengl::GlWindow window("Shader Commands Demo", settings); + auto renderer = window.make_renderer(); + + auto shaderdir = path / "assets" / "test" / "shaders"; + + // Initialize shader template + resources::ShaderTemplate frag_template(shaderdir / "demo_7_shader_command.frag.glsl"); + + // Load snippets from a snippet directory + frag_template.load_snippets(shaderdir / "demo_7_snippets"); + + auto vert_shader_file = (shaderdir / "demo_7_shader_command.vert.glsl").open(); + auto vert_shader_src = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::vertex, + vert_shader_file.read()); + vert_shader_file.close(); + + auto frag_shader_src = frag_template.generate_source(); + + auto shader = renderer->add_shader({vert_shader_src, frag_shader_src}); + + // Create a simple quad for rendering + auto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad()); + + auto uniforms = shader->new_uniform_input("time", 0.0f); + + Renderable display_obj{ + uniforms, + quad, + false, + false, + }; + + if (not check_uniform_completeness({display_obj})) { + log::log(WARN << "Uniforms not complete."); + } + + auto pass = renderer->add_render_pass({display_obj}, renderer->get_display_target()); + + // Main loop + float time = 0.0f; + while (not window.should_close()) { + time += 0.016f; + uniforms->update("time", time); + + renderer->render(pass); + window.update(); + qtapp->process_events(); + + renderer->check_error(); + } + window.close(); +} + +} // 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..7642fe06c8 --- /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 { + +/** + * Demonstrate the shader template system for shader generation. + * - Window creation + * - Create a shader template + * - Load shader snippets (command) from files + * - Generate shader sources from the template + * - Creating a render pass + * - Creating a renderable from a mesh + * + * @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/resources/CMakeLists.txt b/libopenage/renderer/resources/CMakeLists.txt index c5946910e5..16a0e5eb5f 100644 --- a/libopenage/renderer/resources/CMakeLists.txt +++ b/libopenage/renderer/resources/CMakeLists.txt @@ -4,6 +4,7 @@ add_sources(libopenage mesh_data.cpp palette_info.cpp shader_source.cpp + shader_template.cpp texture_data.cpp texture_info.cpp texture_subinfo.cpp diff --git a/libopenage/renderer/resources/shader_template.cpp b/libopenage/renderer/resources/shader_template.cpp new file mode 100644 index 0000000000..c328ba82bc --- /dev/null +++ b/libopenage/renderer/resources/shader_template.cpp @@ -0,0 +1,75 @@ +// Copyright 2024-2025 the openage authors. See copying.md for legal info. + +#include "shader_template.h" + +#include + +#include "error/error.h" +#include "log/log.h" + +namespace openage::renderer::resources { + +ShaderTemplate::ShaderTemplate(const util::Path &template_path) { + auto file = template_path.open(); + this->template_code = file.read(); + file.close(); + + std::string marker = "// PLACEHOLDER: "; + size_t pos = 0; + + while ((pos = this->template_code.find(marker, pos)) != std::string::npos) { + size_t name_start = pos + marker.length(); + size_t line_end = this->template_code.find('\n', name_start); + std::string name = this->template_code.substr(name_start, line_end - name_start); + // Trim trailing whitespace (space, tab, carriage return, etc.) + name.erase(name.find_last_not_of(" \t\r\n") + 1); + + this->placeholders.push_back({name, pos, line_end - pos}); + pos = line_end; + } +} + +void ShaderTemplate::load_snippets(const util::Path &snippet_path) { + // load config here + util::Path snippet_path_copy = snippet_path; + for (const auto &entry : snippet_path_copy.iterdir()) { + if (entry.get_name().ends_with(".snippet")) { + add_snippet(entry); + } + } +} + +void ShaderTemplate::add_snippet(const util::Path &snippet_path) { + auto file = snippet_path.open(); + std::string content = file.read(); + file.close(); + + std::string name = snippet_path.get_stem(); + + snippets[name] = content; +} + +renderer::resources::ShaderSource ShaderTemplate::generate_source() const { + std::string result_src = template_code; + + // Replace placeholders in reverse order (to avoid offset issues) + for (auto it = placeholders.rbegin(); it != placeholders.rend(); ++it) { + const auto &ph = *it; + auto snippet_it = snippets.find(ph.name); + if (snippet_it != snippets.end()) { + result_src.replace(ph.position, ph.length, snippet_it->second); + } + else { + throw Error(MSG(err) << "Missing snippet for placeholder: " << ph.name); + } + } + + auto result = resources::ShaderSource( + resources::shader_lang_t::glsl, + resources::shader_stage_t::fragment, + std::move(result_src)); + + return result; +} + +} // namespace openage::renderer::world diff --git a/libopenage/renderer/resources/shader_template.h b/libopenage/renderer/resources/shader_template.h new file mode 100644 index 0000000000..3e6656629e --- /dev/null +++ b/libopenage/renderer/resources/shader_template.h @@ -0,0 +1,73 @@ +// Copyright 2024-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include + +#include "renderer/resources/shader_source.h" +#include "util/path.h" + +namespace openage { +namespace renderer { +namespace resources { + +/** + * Manages shader templates and their code snippets. + * Allows loading configurable shader commands and generating + * complete shader source code. + */ +class ShaderTemplate { +public: + /** + * Create a shader template from source code of shader. + * + * @param template_path Path to the template file. + */ + explicit ShaderTemplate(const util::Path &template_path); + + /** + * Load all snippets from a directory of .snippet files. + * + * @param snippet_path Path to directory containing snippet files. + */ + void load_snippets(const util::Path &snippet_path); + + /** + * Add a single code snippet to snippets map. + * + * @param name Snippet identifier. + * @param snippet_path Path to the snippet file. + */ + void add_snippet(const util::Path &snippet_path); + + /** + * Generate final shader source code with all snippets inserted. + * + * @return Complete shader code. + * @throws Error if any required placeholders are missing snippets. + */ + renderer::resources::ShaderSource generate_source() const; + +private: + /// Original template code with placeholders + std::string template_code; + /// Mapping of placeholder IDs to their code snippets + std::map snippets; + + /// Info about a placeholder found in the template + struct Placeholder { + std::string name; + size_t position; + size_t length; + }; + + /// All placeholders found in the template + /// precomputed on creation + std::vector placeholders; +}; +} // namespace world +} // namespace renderer +} // namespace openage diff --git a/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index d8d93279af..eec13b602b 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" diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index f1256f3b57..7a0fe02a4c 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