// Modern effects for a modern Streamer // Copyright (C) 2017 Michael Fabian Dirks // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #include "gfx-shader.hpp" #include #include #include #include "plugin.hpp" #define ST "Shader" #define ST_SHADER ST ".Shader" #define ST_SHADER_FILE ST_SHADER ".File" #define ST_SHADER_TECHNIQUE ST_SHADER ".Technique" #define ST_SHADER_SIZE ST_SHADER ".Size" #define ST_SHADER_SIZE_WIDTH ST_SHADER_SIZE ".Width" #define ST_SHADER_SIZE_HEIGHT ST_SHADER_SIZE ".Height" #define ST_PARAMETERS ST ".Parameters" gfx::shader::shader::shader(obs_source_t* self, shader_mode mode) : _self(self), _mode(mode), _base_width(1), _base_height(1), _input_a(), _input_b(), _shader(), _shader_file(), _shader_tech("Draw"), _shader_file_mt(), _shader_file_sz(), _shader_file_tick(0), _shader_params_invalid(true), _width_type(size_type::Percent), _width_value(1.0), _height_type(size_type::Percent), _height_value(1.0), _time(0), _random() { _random.seed(static_cast(time(NULL))); } gfx::shader::shader::~shader() {} bool gfx::shader::shader::load_shader(std::filesystem::path file) { if (!is_shader_different(file)) return false; _shader = gs::effect(file); _shader_file = file; _shader_file_mt = std::filesystem::last_write_time(file); _shader_file_sz = std::filesystem::file_size(file); _shader_file_tick = 0; _shader_params_invalid = true; return true; } void gfx::shader::shader::load_shader_params() { if (!_shader_params_invalid) return; _shader_params.clear(); if (!_shader) return; if (gs::effect_technique tech = _shader.get_technique(_shader_tech); tech != nullptr) { for (size_t idx = 0; idx < tech.count_passes(); idx++) { auto pass = tech.get_pass(idx); for (size_t vidx = 0; vidx < pass.count_vertex_parameters(); vidx++) { auto el = pass.get_vertex_parameter(vidx); auto fnd = _shader_params.find(el.get_name()); if (fnd != _shader_params.end()) continue; auto param = gfx::shader::parameter::make_parameter(el, ST_PARAMETERS); if (param) _shader_params.insert_or_assign(el.get_name(), param); } for (size_t vidx = 0; vidx < pass.count_pixel_parameters(); vidx++) { auto el = pass.get_pixel_parameter(vidx); auto fnd = _shader_params.find(el.get_name()); if (fnd != _shader_params.end()) continue; auto param = gfx::shader::parameter::make_parameter(el, ST_PARAMETERS); if (param) _shader_params.insert_or_assign(el.get_name(), param); } } } _shader_params_invalid = false; } void gfx::shader::shader::properties(obs_properties_t* pr) { { auto grp = obs_properties_create(); obs_properties_add_group(pr, ST_SHADER, D_TRANSLATE(ST_SHADER), OBS_GROUP_NORMAL, grp); { auto p = obs_properties_add_path(grp, ST_SHADER_FILE, D_TRANSLATE(ST_SHADER_FILE), OBS_PATH_FILE, "*.*", nullptr); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_SHADER_FILE))); obs_property_set_modified_callback2( p, [](void* priv, obs_properties_t* props, obs_property_t* prop, obs_data_t* data) noexcept { return reinterpret_cast(priv)->on_shader_changed(props, prop, data); }, this); } { auto p = obs_properties_add_list(grp, ST_SHADER_TECHNIQUE, D_TRANSLATE(ST_SHADER_TECHNIQUE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_SHADER_TECHNIQUE))); obs_property_set_modified_callback2( p, [](void* priv, obs_properties_t* props, obs_property_t* prop, obs_data_t* data) noexcept { return reinterpret_cast(priv)->on_technique_changed(props, prop, data); }, this); } if (_mode != shader_mode::Transition) { auto grp2 = obs_properties_create(); obs_properties_add_group(grp, ST_SHADER_SIZE, D_TRANSLATE(ST_SHADER_SIZE), OBS_GROUP_NORMAL, grp2); { auto p = obs_properties_add_text(grp2, ST_SHADER_SIZE_WIDTH, D_TRANSLATE(ST_SHADER_SIZE_WIDTH), OBS_TEXT_DEFAULT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_SHADER_SIZE_WIDTH))); } { auto p = obs_properties_add_text(grp2, ST_SHADER_SIZE_HEIGHT, D_TRANSLATE(ST_SHADER_SIZE_HEIGHT), OBS_TEXT_DEFAULT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_SHADER_SIZE_HEIGHT))); } } } { auto grp = obs_properties_create(); obs_properties_add_group(pr, ST_PARAMETERS, D_TRANSLATE(ST_PARAMETERS), OBS_GROUP_NORMAL, grp); } } bool gfx::shader::shader::is_shader_different(std::filesystem::path file) { // Check if the file name differs. if (file != _shader_file) return true; // Is the file write time different? if (std::filesystem::last_write_time(_shader_file) != _shader_file_mt) { return true; } // Is the file size different? if (std::filesystem::file_size(_shader_file) != _shader_file_sz) { return true; } return false; } bool gfx::shader::shader::on_shader_changed(obs_properties_t* props, obs_property_t* prop, obs_data_t* data) { // Load changed shader. update_shader(data); // Clear list of techniques. obs_property_t* list = obs_properties_get(props, ST_SHADER_TECHNIQUE); obs_property_list_clear(list); // Don't go further if there is no shader. if (!_shader) return true; // Rebuild Technique list. { const char* tech_name_c = obs_data_get_string(data, ST_SHADER_TECHNIQUE); std::string tech_name = tech_name_c ? tech_name_c : ""; bool have_tech = false; for (size_t idx = 0, idx_end = _shader.count_techniques(); idx < idx_end; idx++) { auto tech = _shader.get_technique(idx); obs_property_list_add_string(list, tech.name().c_str(), tech.name().c_str()); if (tech.name() == tech_name) { have_tech = true; } } if (!have_tech && (_shader.count_techniques() > 0)) { obs_data_set_string(data, ST_SHADER_TECHNIQUE, _shader.get_technique(0).name().c_str()); //on_technique_changed(props, prop, data); } else if (_shader.count_techniques() == 0) { obs_data_set_string(data, ST_SHADER_TECHNIQUE, ""); } } return true; } bool gfx::shader::shader::on_technique_changed(obs_properties_t* props, obs_property_t* prop, obs_data_t* data) { // Clear parameter options. auto grp = obs_property_group_content(obs_properties_get(props, ST_PARAMETERS)); while (true) { if (auto p = obs_properties_first(grp); p != nullptr) { std::string name = obs_property_name(p) ? obs_property_name(p) : ""; obs_properties_remove_by_name(grp, name.c_str()); } else { break; } } // Don't go further if there is no shader. if (!_shader) return true; // Load technique. update_technique(data); // Rebuild new parameters. for (auto kv : _shader_params) { kv.second->properties(grp, data); kv.second->update(data); } return true; } void gfx::shader::shader::update_shader(obs_data_t* data) { { const char* file_c = obs_data_get_string(data, ST_SHADER_FILE); std::string file = file_c ? file_c : ""; if (file != "") { try { if (!load_shader(file)) return; } catch (const std::exception& ex) { P_LOG_ERROR("Failed to load shader: %s.", ex.what()); _shader.reset(); } catch (...) { P_LOG_ERROR("Failed to load shader."); _shader.reset(); } } else { _shader.reset(); } } } void gfx::shader::shader::update_technique(obs_data_t* data) { const char* shader_tech_c = obs_data_get_string(data, ST_SHADER_TECHNIQUE); std::string shader_tech = shader_tech_c ? shader_tech_c : ""; if ((_shader_params_invalid) || (_shader_tech != shader_tech)) { _shader_tech = shader_tech; _shader_params_invalid = true; load_shader_params(); } } inline std::pair parse_text_as_size(const char* text) { double_t v = 0; if (sscanf(text, "%lf", &v) == 1) { const char* prc_chr = strrchr(text, '%'); if (prc_chr && (*prc_chr == '%')) { return {gfx::shader::size_type::Percent, v / 100.0}; } else { return {gfx::shader::size_type::Pixel, v}; } } else { return {gfx::shader::size_type::Percent, 1.0}; } } void gfx::shader::shader::update(obs_data_t* data) { update_shader(data); update_technique(data); { auto sz_x = parse_text_as_size(obs_data_get_string(data, ST_SHADER_SIZE_WIDTH)); _width_type = sz_x.first; _width_value = std::clamp(sz_x.second, 0.01, 8192.0); auto sz_y = parse_text_as_size(obs_data_get_string(data, ST_SHADER_SIZE_HEIGHT)); _height_type = sz_y.first; _height_value = std::clamp(sz_y.second, 0.01, 8192.0); } for (auto kv : _shader_params) { kv.second->update(data); } } uint32_t gfx::shader::shader::width() { switch (_mode) { case shader_mode::Transition: return _base_width; case shader_mode::Source: switch (_width_type) { case size_type::Pixel: return std::clamp(static_cast(_width_value), 1u, 8192u); case size_type::Percent: return std::clamp(static_cast(_width_value * _base_width), 1u, 8192u); } case shader_mode::Filter: switch (_width_type) { case size_type::Pixel: return std::clamp(static_cast(_width_value), 1u, 8192u); case size_type::Percent: if (_input_a) { return std::clamp(static_cast(_width_value * _input_a->get_width()), 1u, 8192u); } else { return std::clamp(static_cast(_width_value * _base_width), 1u, 8192u); } } default: return 0; } } uint32_t gfx::shader::shader::height() { switch (_mode) { case shader_mode::Transition: return _base_height; case shader_mode::Source: switch (_height_type) { case size_type::Pixel: return std::clamp(static_cast(_height_value), 1u, 8192u); case size_type::Percent: return std::clamp(static_cast(_height_value * _base_height), 1u, 8192u); } case shader_mode::Filter: switch (_height_type) { case size_type::Pixel: return std::clamp(static_cast(_height_value), 1u, 8192u); case size_type::Percent: if (_input_a) { return std::clamp(static_cast(_height_value * _input_a->get_height()), 1u, 8192u); } else { return std::clamp(static_cast(_height_value * _base_height), 1u, 8192u); } } default: return 0; } } bool gfx::shader::shader::tick(float_t time) { // Update State _time += time; return false; } void gfx::shader::shader::render() { if (!_shader) return; uint32_t szw = width(); uint32_t szh = height(); for (auto kv : _shader_params) { kv.second->assign(); } if (gs::effect_parameter el = _shader.get_parameter("Time"); el != nullptr) { if (el.get_type() == gs::effect_parameter::type::Float4) { el.set_float4( _time, 0, 0, static_cast(static_cast(_random()) / static_cast(std::numeric_limits::max()))); } } while (gs_effect_loop(_shader.get_object(), _shader_tech.c_str())) { gs_draw_sprite(nullptr, 0, szw, szh); } } void gfx::shader::shader::set_size(uint32_t w, uint32_t h) { _base_width = w; _base_height = h; } void gfx::shader::shader::set_input_a(std::shared_ptr tex) { _input_a = tex; } void gfx::shader::shader::set_input_b(std::shared_ptr tex) { _input_b = tex; }