From 26b447901cc056deab01d708257f4c1a227dc6f4 Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Mon, 6 Nov 2017 12:36:01 +0100 Subject: [PATCH] filter-custom-shader: Implement new Filter 'Custom Shader' Custom Shader allows you to write your own effect files and just have them applied to your source(s). It will dynamically update the properties to match the parameters in the source as well as offer some special parameters to the shader. # Conflicts: # CMakeLists.txt # data/locale/en-US.ini # Conflicts: # data/locale/en-US.ini --- CMakeLists.txt | 2 + data/locale/en-US.ini | 8 + source/filter-custom-shader.cpp | 409 ++++++++++++++++++++++++++++++++ source/filter-custom-shader.h | 111 +++++++++ 4 files changed, 530 insertions(+) create mode 100644 source/filter-custom-shader.cpp create mode 100644 source/filter-custom-shader.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3474f7c7..f65534bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ SET(obs-stream-effects_HEADERS "${PROJECT_SOURCE_DIR}/source/filter-blur.h" "${PROJECT_SOURCE_DIR}/source/filter-shape.h" "${PROJECT_SOURCE_DIR}/source/filter-transform.h" + "${PROJECT_SOURCE_DIR}/source/filter-custom-shader.h" "${PROJECT_SOURCE_DIR}/source/source-mirror.h" "${PROJECT_SOURCE_DIR}/source/gs-helper.h" "${PROJECT_SOURCE_DIR}/source/gs-effect.h" @@ -58,6 +59,7 @@ SET(obs-stream-effects_SOURCES "${PROJECT_SOURCE_DIR}/source/filter-blur.cpp" "${PROJECT_SOURCE_DIR}/source/filter-shape.cpp" "${PROJECT_SOURCE_DIR}/source/filter-transform.cpp" + "${PROJECT_SOURCE_DIR}/source/filter-custom-shader.cpp" "${PROJECT_SOURCE_DIR}/source/source-mirror.cpp" "${PROJECT_SOURCE_DIR}/source/gs-helper.cpp" "${PROJECT_SOURCE_DIR}/source/gs-effect.cpp" diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index d912413c..b90e65e9 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -14,6 +14,14 @@ Filter.Blur.Bilateral.Smoothing="Smoothing" Filter.Blur.Bilateral.Sharpness="Sharpness" Filter.Blur.ColorFormat="Color Format" +# Filter - Custom Shader +Filter.CustomShader="Custom Shader" +Filter.CustomShader.Type="Type" +Filter.CustomShader.Type.Text="Text Input" +Filter.CustomShader.Type.File="File Input" +Filter.CustomShader.Content.Text="Effect Text" +Filter.CustomShader.Content.File="Effect File" + # Filter - Displacement Filter.Displacement="Displacement Mapping" Filter.Displacement.File="File" diff --git a/source/filter-custom-shader.cpp b/source/filter-custom-shader.cpp new file mode 100644 index 00000000..453e7e94 --- /dev/null +++ b/source/filter-custom-shader.cpp @@ -0,0 +1,409 @@ +/* + * 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 "filter-custom-shader.h" +#include "strings.h" +#include +#include + +extern "C" { +#pragma warning (push) +#pragma warning (disable: 4201) +#include "libobs/util/platform.h" +#include "libobs/graphics/graphics.h" +#include "libobs/graphics/matrix4.h" +#pragma warning (pop) +} + +#define S "Filter.CustomShader" +#define S_TYPE "Filter.CustomShader.Type" +#define S_TYPE_TEXT "Filter.CustomShader.Type.Text" +#define S_TYPE_FILE "Filter.CustomShader.Type.File" +#define S_CONTENT_TEXT "Filter.CustomShader.Content.Text" +#define S_CONTENT_FILE "Filter.CustomShader.Content.File" + +#define DEFAULT_SHADER "" + +/* Special Parameters + +Default: +- ViewSize - View Resolution, float2 +- ViewSizeI - View Resolution, integer2 +- Pass - Current Pass Index, integer + +Texture2d: +- $(param)_size - Texture Size, float2 +- $(param)_isize - Texture Size, integer2 +- $(param)_texel - Texture Texel Size, float2 + +*/ + +enum class ShaderType : int64_t { + Text, + File +}; + +static Filter::CustomShader* handler; +INITIALIZER(HandlerInit) { + initializerFunctions.push_back([] { + handler = new Filter::CustomShader(); + }); + finalizerFunctions.push_back([] { + delete handler; + }); +} + +Filter::CustomShader::CustomShader() { + memset(&sourceInfo, 0, sizeof(obs_source_info)); + sourceInfo.id = "obs-stream-effects-filter-custom-shader"; + sourceInfo.type = OBS_SOURCE_TYPE_FILTER; + sourceInfo.output_flags = OBS_SOURCE_VIDEO; + sourceInfo.get_name = get_name; + sourceInfo.get_defaults = get_defaults; + sourceInfo.get_properties = get_properties; + + sourceInfo.create = create; + sourceInfo.destroy = destroy; + sourceInfo.update = update; + sourceInfo.activate = activate; + sourceInfo.deactivate = deactivate; + sourceInfo.video_tick = video_tick; + sourceInfo.video_render = video_render; + + obs_register_source(&sourceInfo); +} + +Filter::CustomShader::~CustomShader() {} + +const char * Filter::CustomShader::get_name(void *) { + return P_TRANSLATE(S); +} + +void Filter::CustomShader::get_defaults(obs_data_t *data) { + obs_data_set_default_int(data, S_TYPE, (int64_t)ShaderType::Text); + obs_data_set_default_string(data, S_CONTENT_TEXT, DEFAULT_SHADER); +} + +obs_properties_t * Filter::CustomShader::get_properties(void *ptr) { + obs_properties_t *pr = obs_properties_create(); + obs_property_t *p; + + p = obs_properties_add_list(pr, S_TYPE, P_TRANSLATE(S_TYPE), obs_combo_type::OBS_COMBO_TYPE_LIST, + obs_combo_format::OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, P_TRANSLATE(S_TYPE_TEXT), (int64_t)ShaderType::Text); + obs_property_list_add_int(p, P_TRANSLATE(S_TYPE_FILE), (int64_t)ShaderType::File); + obs_property_set_modified_callback(p, modified_properties); + + p = obs_properties_add_text(pr, S_CONTENT_TEXT, P_TRANSLATE(S_CONTENT_TEXT), + obs_text_type::OBS_TEXT_MULTILINE); + + std::string defaultPath = "C:\\"; + if (ptr) + defaultPath = reinterpret_cast(ptr)->get_shader_file(); + p = obs_properties_add_path(pr, S_CONTENT_FILE, P_TRANSLATE(S_CONTENT_FILE), + obs_path_type::OBS_PATH_FILE, "OBS Studio Effect (*.effect);;Any File (*.*)", defaultPath.c_str()); + + if (ptr) + reinterpret_cast(ptr)->get_properties(pr); + + return pr; +} + +bool Filter::CustomShader::modified_properties(obs_properties_t *pr, obs_property_t *p, obs_data_t *data) { + ShaderType shaderType = (ShaderType)obs_data_get_int(data, S_TYPE); + switch (shaderType) { + case ShaderType::Text: + obs_property_set_visible(obs_properties_get(pr, S_CONTENT_TEXT), true); + obs_property_set_visible(obs_properties_get(pr, S_CONTENT_FILE), false); + break; + case ShaderType::File: + obs_property_set_visible(obs_properties_get(pr, S_CONTENT_TEXT), false); + obs_property_set_visible(obs_properties_get(pr, S_CONTENT_FILE), true); + break; + } + + return true; +} + +void * Filter::CustomShader::create(obs_data_t *data, obs_source_t *src) { + return new Instance(data, src); +} + +void Filter::CustomShader::destroy(void *ptr) { + delete reinterpret_cast(ptr); +} + +uint32_t Filter::CustomShader::get_width(void *ptr) { + return reinterpret_cast(ptr)->get_width(); +} + +uint32_t Filter::CustomShader::get_height(void *ptr) { + return reinterpret_cast(ptr)->get_height(); +} + +void Filter::CustomShader::update(void *ptr, obs_data_t *data) { + reinterpret_cast(ptr)->update(data); +} + +void Filter::CustomShader::activate(void *ptr) { + reinterpret_cast(ptr)->activate(); +} + +void Filter::CustomShader::deactivate(void *ptr) { + reinterpret_cast(ptr)->activate(); +} + +void Filter::CustomShader::video_tick(void *ptr, float time) { + reinterpret_cast(ptr)->video_tick(time); + +} + +void Filter::CustomShader::video_render(void *ptr, gs_effect_t *effect) { + reinterpret_cast(ptr)->video_render(effect); +} + +Filter::CustomShader::Instance::Instance(obs_data_t *data, obs_source_t *source) { + m_sourceContext = source; + + update(data); +} + +Filter::CustomShader::Instance::~Instance() { + +} + +void Filter::CustomShader::Instance::update(obs_data_t *data) { + ShaderType shaderType = (ShaderType)obs_data_get_int(data, S_TYPE); + if (shaderType == ShaderType::Text) { + const char* shaderText = obs_data_get_string(data, S_CONTENT_TEXT); + try { + m_effect = GS::Effect(shaderText, "Text Shader"); + } catch (std::runtime_error& ex) { + const char* filterName = obs_source_get_name(m_sourceContext); + P_LOG_ERROR("[%s] Shader loading failed with error(s): %s", filterName, ex.what()); + } + } else if (shaderType == ShaderType::File) { + update_shader_file(obs_data_get_string(data, S_CONTENT_FILE)); + } +} + +uint32_t Filter::CustomShader::Instance::get_width() { + return 0; +} + +uint32_t Filter::CustomShader::Instance::get_height() { + return 0; +} + +void Filter::CustomShader::Instance::activate() { + m_isActive = true; +} + +void Filter::CustomShader::Instance::deactivate() { + m_isActive = false; +} + +void Filter::CustomShader::Instance::video_tick(float time) { + m_shaderFile.lastCheck += time; + if (m_shaderFile.lastCheck >= 0.5) { + update_shader_file(m_shaderFile.filePath); + m_shaderFile.lastCheck = 0; + } +} + +void Filter::CustomShader::Instance::video_render(gs_effect_t *effect) { + if (!m_sourceContext || !m_isActive) { + obs_source_skip_video_filter(m_sourceContext); + return; + } + + obs_source_t *parent = obs_filter_get_parent(m_sourceContext); + obs_source_t *target = obs_filter_get_target(m_sourceContext); + if (!parent || !target) { + obs_source_skip_video_filter(m_sourceContext); + return; + } + + uint32_t baseW = obs_source_get_base_width(target), + baseH = obs_source_get_base_height(target); + if (!baseW || !baseH) { + obs_source_skip_video_filter(m_sourceContext); + return; + } + + // Temporary + obs_source_skip_video_filter(m_sourceContext); +} + +void Filter::CustomShader::Instance::get_properties(obs_properties_t *pr) { + if (m_effect.GetObject() == nullptr) + return; + + m_effectParameters.clear(); + for (auto p : m_effect.GetParameters()) { + std::string p_name = p.GetName(); + std::string p_desc = p_name; + + if (IsSpecialParameter(p_name, p.GetType())) + continue; + + Parameter prm; + prm.origName = p_name; + prm.origType = p.GetType(); + + switch (p.GetType()) { + case GS::EffectParameter::Type::Boolean: + { + prm.names.push_back(p_name); + prm.descs.push_back(p_desc); + m_effectParameters.emplace_back(prm); + obs_properties_add_bool(pr, m_effectParameters.back().names[0].c_str(), m_effectParameters.back().descs[0].c_str()); + break; + } + case GS::EffectParameter::Type::Float: + case GS::EffectParameter::Type::Float2: + case GS::EffectParameter::Type::Float3: + case GS::EffectParameter::Type::Float4: + { + prm.names.push_back(p_name + "0"); + prm.descs.push_back(p_desc + "[0]"); + if (p.GetType() >= GS::EffectParameter::Type::Float2) { + prm.names.push_back(p_name + "1"); + prm.descs.push_back(p_desc + "[1]"); + } + if (p.GetType() >= GS::EffectParameter::Type::Float3) { + prm.names.push_back(p_name + "2"); + prm.descs.push_back(p_desc + "[2]"); + } + if (p.GetType() >= GS::EffectParameter::Type::Float4) { + prm.names.push_back(p_name + "3"); + prm.descs.push_back(p_desc + "[3]"); + } + + m_effectParameters.emplace_back(prm); + + obs_properties_add_float(pr, m_effectParameters.back().names[0].c_str(), m_effectParameters.back().descs[0].c_str(), FLT_MIN, FLT_MAX, FLT_EPSILON); + if (p.GetType() >= GS::EffectParameter::Type::Float2) + obs_properties_add_float(pr, m_effectParameters.back().names[1].c_str(), m_effectParameters.back().descs[1].c_str(), FLT_MIN, FLT_MAX, FLT_EPSILON); + if (p.GetType() >= GS::EffectParameter::Type::Float3) + obs_properties_add_float(pr, m_effectParameters.back().names[2].c_str(), m_effectParameters.back().descs[2].c_str(), FLT_MIN, FLT_MAX, FLT_EPSILON); + if (p.GetType() >= GS::EffectParameter::Type::Float4) + obs_properties_add_float(pr, m_effectParameters.back().names[3].c_str(), m_effectParameters.back().descs[3].c_str(), FLT_MIN, FLT_MAX, FLT_EPSILON); + + break; + } + case GS::EffectParameter::Type::Integer: + case GS::EffectParameter::Type::Integer2: + case GS::EffectParameter::Type::Integer3: + case GS::EffectParameter::Type::Integer4: + { + prm.names.push_back(p_name + "0"); + prm.descs.push_back(p_desc + "[0]"); + if (p.GetType() >= GS::EffectParameter::Type::Integer2) { + prm.names.push_back(p_name + "1"); + prm.descs.push_back(p_desc + "[1]"); + } + if (p.GetType() >= GS::EffectParameter::Type::Integer3) { + prm.names.push_back(p_name + "2"); + prm.descs.push_back(p_desc + "[2]"); + } + if (p.GetType() >= GS::EffectParameter::Type::Integer4) { + prm.names.push_back(p_name + "3"); + prm.descs.push_back(p_desc + "[3]"); + } + + m_effectParameters.emplace_back(prm); + + obs_properties_add_int(pr, m_effectParameters.back().names[0].c_str(), m_effectParameters.back().descs[0].c_str(), INT_MIN, INT_MAX, FLT_EPSILON); + if (p.GetType() >= GS::EffectParameter::Type::Integer2) + obs_properties_add_int(pr, m_effectParameters.back().names[1].c_str(), m_effectParameters.back().descs[1].c_str(), INT_MIN, INT_MAX, FLT_EPSILON); + if (p.GetType() >= GS::EffectParameter::Type::Integer3) + obs_properties_add_int(pr, m_effectParameters.back().names[2].c_str(), m_effectParameters.back().descs[2].c_str(), INT_MIN, INT_MAX, FLT_EPSILON); + if (p.GetType() >= GS::EffectParameter::Type::Integer4) + obs_properties_add_int(pr, m_effectParameters.back().names[3].c_str(), m_effectParameters.back().descs[3].c_str(), INT_MIN, INT_MAX, FLT_EPSILON); + + break; + } + } + + } + return; +} + +void Filter::CustomShader::Instance::update_shader_file(std::string file) { + bool doRefresh = false; + struct stat stats; + + if (file != m_shaderFile.filePath) { + m_shaderFile.filePath = file; + doRefresh = true; + } else if (os_stat(m_shaderFile.filePath.c_str(), &stats) != 0) { + doRefresh = doRefresh || + (m_shaderFile.createTime != stats.st_ctime) || + (m_shaderFile.modifiedTime != stats.st_mtime); + m_shaderFile.createTime = stats.st_ctime; + m_shaderFile.modifiedTime = stats.st_mtime; + } + + if (doRefresh) { + try { + m_effect = GS::Effect(m_shaderFile.filePath); + } catch (std::runtime_error& ex) { + const char* filterName = obs_source_get_name(m_sourceContext); + P_LOG_ERROR("[%s] Shader loading failed with error(s): %s", filterName, ex.what()); + } + } +} + +std::string Filter::CustomShader::Instance::get_shader_file() { + return m_shaderFile.filePath; +} + +bool Filter::CustomShader::Instance::IsSpecialParameter(std::string name, GS::EffectParameter::Type type) { + if (type == GS::EffectParameter::Type::Matrix) { + if (name == "ViewProj") + return true; + + } else if (type == GS::EffectParameter::Type::Float2) { + if (name == "ViewSize") + return true; + + // Suffix Tests + size_t posUnderscore = name.find_last_of('_'); + if (posUnderscore != std::string::npos) { + std::string firstPart, secondPart; + firstPart = name.substr(0, posUnderscore); + secondPart = name.substr(posUnderscore + 1); + + // Texture Specials + if ((secondPart == "size") || (secondPart == "texel")) { + try { + GS::EffectParameter texParam = m_effect.GetParameter(firstPart); + if (texParam.GetType() == GS::EffectParameter::Type::Texture) + return true; + } catch (...) {} + } + } + } else if (type == GS::EffectParameter::Type::Float) { + if (name == "Time" || name == "TimeActive") + return true; + } + + return false; +} diff --git a/source/filter-custom-shader.h b/source/filter-custom-shader.h new file mode 100644 index 00000000..b199a1fc --- /dev/null +++ b/source/filter-custom-shader.h @@ -0,0 +1,111 @@ +/* + * 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 + */ + +#pragma once +#include "plugin.h" +#include "gs-effect.h" +#include +#include + +namespace Filter { + class CustomShader { + public: + CustomShader(); + ~CustomShader(); + + static const char *get_name(void *); + static void get_defaults(obs_data_t *); + static obs_properties_t *get_properties(void *); + static bool modified_properties(obs_properties_t *, + obs_property_t *, obs_data_t *); + + static void *create(obs_data_t *, obs_source_t *); + static void destroy(void *); + static uint32_t get_width(void *); + static uint32_t get_height(void *); + static void update(void *, obs_data_t *); + static void activate(void *); + static void deactivate(void *); + static void video_tick(void *, float); + static void video_render(void *, gs_effect_t *); + + private: + obs_source_info sourceInfo; + + private: + class Instance { + public: + Instance(obs_data_t*, obs_source_t*); + ~Instance(); + + void update(obs_data_t*); + uint32_t get_width(); + uint32_t get_height(); + void activate(); + void deactivate(); + void video_tick(float); + void video_render(gs_effect_t*); + void get_properties(obs_properties_t *); + + protected: + void update_shader_file(std::string file); + std::string get_shader_file(); + + bool IsSpecialParameter(std::string name, GS::EffectParameter::Type type); + void ApplySpecialParameter(); + + private: + obs_source_t *m_sourceContext; + bool m_isActive = true; + + // Shader + struct { + std::string filePath; + time_t createTime, modifiedTime; + size_t size; + float_t lastCheck; + } m_shaderFile; + GS::Effect m_effect; + struct Parameter { + std::vector names; + std::vector descs; + std::string origName; + GS::EffectParameter::Type origType; + + // Texture Input + bool texInputSource = false; + struct { + std::string path; + time_t timeCreated, timeModified; + size_t fileSize; + float_t lastChecked; + } textureFile; + struct { + obs_source_t* source; + std::pair resolution; + } textureSource; + + }; + std::list m_effectParameters; + + + friend class CustomShader; + }; + }; +}