obs-StreamFX/source/gfx/shader/gfx-shader.cpp
Michael Fabian 'Xaymar' Dirks 79406b0b08 gfx-shader: Additional stable time values
'Time.x' gets inaccurate if OBS Studio is running for more than two hours, therefore we have to do something to fix it. By allowing the shader code to control when things loop using 'Time.y' (0..1) and 'Time.z' (the number of times 'Time.y' wrapped back to 0), a much more stable animation can be achieved.
2020-03-28 22:17:33 +01:00

467 lines
14 KiB
C++

// 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 <algorithm>
#include <cstdio>
#include <cstring>
#include "obs/obs-tools.hpp"
#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),
_shader(), _shader_file(), _shader_tech("Draw"), _shader_file_mt(), _shader_file_sz(), _shader_file_tick(0),
_width_type(size_type::Percent), _width_value(1.0), _height_type(size_type::Percent), _height_value(1.0),
_time(0), _time_loop(0), _loops(0), _random(), _have_current_params(false)
{
_random.seed(static_cast<unsigned long long>(time(NULL)));
}
gfx::shader::shader::~shader() {}
bool gfx::shader::shader::is_shader_different(const std::filesystem::path& file)
try {
if (std::filesystem::exists(file)) {
// Check if the file name differs.
if (file != _shader_file)
return true;
}
if (std::filesystem::exists(_shader_file)) {
// 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;
} catch (const std::exception& ex) {
LOG_ERROR("Loading shader '%s' failed with error: %s", file.c_str(), ex.what());
return false;
}
bool gfx::shader::shader::is_technique_different(const std::string& tech)
{
// Is the technique different?
if (tech != _shader_tech)
return true;
return false;
}
bool gfx::shader::shader::load_shader(const std::filesystem::path& file, const std::string& tech, bool& shader_dirty,
bool& param_dirty)
try {
if (!std::filesystem::exists(file))
return false;
shader_dirty = is_shader_different(file);
param_dirty = is_technique_different(tech) || shader_dirty;
// Update Shader
if (shader_dirty) {
_shader = gs::effect(file);
_shader_file_mt = std::filesystem::last_write_time(file);
_shader_file_sz = std::filesystem::file_size(file);
_shader_file = file;
_shader_file_tick = 0;
}
// Update Params
if (param_dirty) {
auto settings =
std::shared_ptr<obs_data_t>(obs_source_get_settings(_self), [](obs_data_t* p) { obs_data_release(p); });
bool have_valid_tech = false;
for (size_t idx = 0; idx < _shader.count_techniques(); idx++) {
if (_shader.get_technique(idx).name() == tech) {
have_valid_tech = true;
break;
}
}
if (have_valid_tech) {
_shader_tech = tech;
} else {
_shader_tech = _shader.get_technique(0).name();
// Update source data.
obs_data_set_string(settings.get(), ST_SHADER_TECHNIQUE, _shader_tech.c_str());
}
// Clear the shader parameters map and rebuild.
_shader_params.clear();
auto etech = _shader.get_technique(_shader_tech);
for (size_t idx = 0; idx < etech.count_passes(); idx++) {
auto pass = etech.get_pass(idx);
for (size_t vidx = 0; vidx < pass.count_vertex_parameters(); vidx++) {
auto el = pass.get_vertex_parameter(vidx);
if (!el)
continue;
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);
param->defaults(settings.get());
param->update(settings.get());
}
}
for (size_t vidx = 0; vidx < pass.count_pixel_parameters(); vidx++) {
auto el = pass.get_pixel_parameter(vidx);
if (!el)
continue;
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);
param->defaults(settings.get());
param->update(settings.get());
}
}
}
}
return true;
} catch (const std::exception& ex) {
LOG_ERROR("Loading shader '%s' failed with error: %s", file.c_str(), ex.what());
return false;
} catch (...) {
return false;
}
void gfx::shader::shader::properties(obs_properties_t* pr)
{
_have_current_params = false;
{
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<gfx::shader::shader*>(priv)->on_properties_modified(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<gfx::shader::shader*>(priv)->on_properties_modified(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::on_properties_modified(obs_properties_t* props, obs_property_t* prop, obs_data_t* data)
{
bool shader_dirty = false;
bool param_dirty = false;
if (!update_shader(data, shader_dirty, param_dirty))
return false;
{ // Clear list of techniques and rebuild it.
obs_property_t* p_tech_list = obs_properties_get(props, ST_SHADER_TECHNIQUE);
obs_property_list_clear(p_tech_list);
for (size_t idx = 0; idx < _shader.count_techniques(); idx++) {
auto tech = _shader.get_technique(idx);
obs_property_list_add_string(p_tech_list, tech.name().c_str(), tech.name().c_str());
}
}
if (param_dirty || !_have_current_params) {
// Clear parameter options.
auto grp = obs_property_group_content(obs_properties_get(props, ST_PARAMETERS));
for (auto p = obs_properties_first(grp); p != nullptr; p = obs_properties_first(grp)) {
obs::tools::obs_properties_remove_by_name(grp, obs_property_name(p));
}
// Rebuild new parameters.
for (auto kv : _shader_params) {
kv.second->properties(grp, data);
kv.second->defaults(data);
kv.second->update(data);
}
}
_have_current_params = true;
return shader_dirty || param_dirty || !_have_current_params;
}
bool gfx::shader::shader::update_shader(obs_data_t* data, bool& shader_dirty, bool& param_dirty)
{
const char* file_c = obs_data_get_string(data, ST_SHADER_FILE);
std::string file = file_c ? file_c : "";
const char* tech_c = obs_data_get_string(data, ST_SHADER_TECHNIQUE);
std::string tech = tech_c ? tech_c : "Draw";
return load_shader(file, tech, shader_dirty, param_dirty);
}
inline std::pair<gfx::shader::size_type, double_t> 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)
{
bool v1, v2;
update_shader(data, v1, v2);
{
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<uint32_t>(_width_value), 1u, 8192u);
case size_type::Percent:
return std::clamp(static_cast<uint32_t>(_width_value * _base_width), 1u, 8192u);
}
case shader_mode::Filter:
switch (_width_type) {
case size_type::Pixel:
return std::clamp(static_cast<uint32_t>(_width_value), 1u, 8192u);
case size_type::Percent:
return std::clamp(static_cast<uint32_t>(_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<uint32_t>(_height_value), 1u, 8192u);
case size_type::Percent:
return std::clamp(static_cast<uint32_t>(_height_value * _base_height), 1u, 8192u);
}
case shader_mode::Filter:
switch (_height_type) {
case size_type::Pixel:
return std::clamp(static_cast<uint32_t>(_height_value), 1u, 8192u);
case size_type::Percent:
return std::clamp(static_cast<uint32_t>(_height_value * _base_height), 1u, 8192u);
}
default:
return 0;
}
}
bool gfx::shader::shader::tick(float_t time)
{
_shader_file_tick = static_cast<float_t>(static_cast<double_t>(_shader_file_tick) + static_cast<double_t>(time));
if (_shader_file_tick >= 1.0f / 3.0f) {
_shader_file_tick -= 1.0f / 3.0f;
bool v1, v2;
load_shader(_shader_file, _shader_tech, v1, v2);
}
// Update State
_time += time;
_time_loop += time;
if (_time_loop > 1.) {
_time_loop -= 1.;
// Loops
_loops += 1;
if (_loops >= 4194304)
_loops = -_loops;
}
return false;
}
void gfx::shader::shader::prepare_render()
{
if (!_shader)
return;
for (auto kv : _shader_params) {
kv.second->assign();
}
// Time: (Current Time), (Zero), (Zero), (Random Value)
if (gs::effect_parameter el = _shader.get_parameter("Time"); el != nullptr) {
if (el.get_type() == gs::effect_parameter::type::Float4) {
el.set_float4(
_time, _time_loop, static_cast<float_t>(_loops),
static_cast<float_t>(static_cast<double_t>(_random())
/ static_cast<double_t>(std::numeric_limits<unsigned long long>::max())));
}
}
// ViewSize: (Width), (Height), (1.0 / Width), (1.0 / Height)
if (auto el = _shader.get_parameter("ViewSize"); el != nullptr) {
if (el.get_type() == gs::effect_parameter::type::Float4) {
el.set_float4(static_cast<float_t>(width()), static_cast<float_t>(height()),
1.0f / static_cast<float_t>(width()), 1.0f / static_cast<float_t>(height()));
}
}
}
void gfx::shader::shader::render()
{
if (!_shader)
return;
while (gs_effect_loop(_shader.get_object(), _shader_tech.c_str())) {
gs_draw_sprite(nullptr, 0, width(), height());
}
}
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<gs::texture> tex)
{
if (!_shader)
return;
if (gs::effect_parameter el = _shader.get_parameter("InputA"); el != nullptr) {
if (el.get_type() == gs::effect_parameter::type::Texture) {
el.set_texture(tex);
}
}
}
void gfx::shader::shader::set_input_b(std::shared_ptr<gs::texture> tex)
{
if (!_shader)
return;
if (gs::effect_parameter el = _shader.get_parameter("InputB"); el != nullptr) {
if (el.get_type() == gs::effect_parameter::type::Texture) {
el.set_texture(tex);
}
}
}
void gfx::shader::shader::set_transition_time(float_t t)
{
if (!_shader)
return;
if (gs::effect_parameter el = _shader.get_parameter("TransitionTime"); el != nullptr) {
if (el.get_type() == gs::effect_parameter::type::Float) {
el.set_float(t);
}
}
}
void gfx::shader::shader::set_transition_size(uint32_t w, uint32_t h)
{
if (!_shader)
return;
if (gs::effect_parameter el = _shader.get_parameter("TransitionSize"); el != nullptr) {
if (el.get_type() == gs::effect_parameter::type::Integer2) {
el.set_int2(w, h);
}
}
}