From 9e7894c9c30e880734b2c00aaaf28915f085c6ce Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Sat, 27 Apr 2019 16:10:16 +0200 Subject: [PATCH] filter-dynamic-mask: Implement Dynamic Mask filter This filter allows the use of another source as a mask, allowing complex filter graphs and trippy effects, such as creating a text source with three animated videos, each using a different color channel as the mask. Fixes #18. --- CMakeLists.txt | 3 + data/effects/channel-mask.effect | 113 ++++++ data/locale/en-US.ini | 19 + source/filters/filter-dynamic-mask.cpp | 506 +++++++++++++++++++++++++ source/filters/filter-dynamic-mask.hpp | 111 ++++++ source/strings.hpp | 10 + 6 files changed, 762 insertions(+) create mode 100644 data/effects/channel-mask.effect create mode 100644 source/filters/filter-dynamic-mask.cpp create mode 100644 source/filters/filter-dynamic-mask.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cbc1a70..ac264a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -221,6 +221,7 @@ SET(PROJECT_DATA_LOCALE "${PROJECT_SOURCE_DIR}/data/locale/en-US.ini" ) SET(PROJECT_DATA_EFFECTS + "${PROJECT_SOURCE_DIR}/data/effects/channel-mask.effect" "${PROJECT_SOURCE_DIR}/data/effects/color-conversion.effect" "${PROJECT_SOURCE_DIR}/data/effects/displace.effect" "${PROJECT_SOURCE_DIR}/data/effects/mask.effect" @@ -339,6 +340,8 @@ SET(PROJECT_PRIVATE_SOURCE "${PROJECT_SOURCE_DIR}/source/filters/filter-custom-shader.cpp" "${PROJECT_SOURCE_DIR}/source/filters/filter-displacement.hpp" "${PROJECT_SOURCE_DIR}/source/filters/filter-displacement.cpp" + "${PROJECT_SOURCE_DIR}/source/filters/filter-dynamic-mask.hpp" + "${PROJECT_SOURCE_DIR}/source/filters/filter-dynamic-mask.cpp" "${PROJECT_SOURCE_DIR}/source/filters/filter-sdf-effects.hpp" "${PROJECT_SOURCE_DIR}/source/filters/filter-sdf-effects.cpp" "${PROJECT_SOURCE_DIR}/source/filters/filter-transform.hpp" diff --git a/data/effects/channel-mask.effect b/data/effects/channel-mask.effect new file mode 100644 index 0000000..9224c6d --- /dev/null +++ b/data/effects/channel-mask.effect @@ -0,0 +1,113 @@ +// -------------------------------------------------------------------------------- // +// Parameters +/// Provided by OBS Studio. +uniform float4x4 ViewProj < + bool visible = false; + bool disabled = true; +>; + +uniform texture2d pMaskInputA < + string name = "Mask Input A"; + string description = "Input to mask."; +>; +uniform texture2d pMaskInputB < + string name = "Mask Input B"; + string description = "Input to use as the mask."; +>; + +uniform float4 pMaskBase < + string name = "Channel Base"; +// float4 minimum = float4(-100.0, -100.0, -100.0, -100.0); +// float4 maximum = float4(100.0, 100.0, 100.0, 100.0); +// float4 default = float4(1, 1, 1, 1); +>; +uniform float4x4 pMaskMatrix < + string name = "Channel Matrix"; +// float4 minimum = float4x4( +// -100, -100, -100, -100, +// -100, -100, -100, -100, +// -100, -100, -100, -100, +// -100, -100, -100, -100); +// float4 maximum = float4x4( +// 100, 100, 100, 100, +// 100, 100, 100, 100, +// 100, 100, 100, 100, +// 100, 100, 100, 100); +// float4 default = float4x4( +// 1, 0, 0, 0, +// 0, 1, 0, 0, +// 0, 0, 1, 0, +// 0, 0, 0, 1); +>; +uniform float4 pMaskMultiplier < + string name = "Channel Multiplier"; +// float4 minimum = float4(-100, -100, -100, -100); +// float4 maximum = float4(100, 100, 100, 100); +// float4 default = float4(1, 1, 1, 1); +>; +// -------------------------------------------------------------------------------- // + +// -------------------------------------------------------------------------------- // +// Samplers +sampler_state maskSamplerA { + Filter = Linear; + AddressU = Clamp; + AddressV = Clamp; +}; + +sampler_state maskSamplerB { + Filter = Linear; + AddressU = Clamp; + AddressV = Clamp; +}; +// -------------------------------------------------------------------------------- // + +// -------------------------------------------------------------------------------- // +// Default Vertex Shader +struct VertDataIn { + float4 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +struct VertDataOut { + float4 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +VertDataOut VSDefault(VertDataIn v_in) +{ + VertDataOut vert_out; + vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); + vert_out.uv = v_in.uv; + return vert_out; +} +// -------------------------------------------------------------------------------- // + +// -------------------------------------------------------------------------------- // +// Channel Masking + +float4 PSChannelMask(VertDataOut v_in) : TARGET +{ + float4 imageA = pMaskInputA.Sample(maskSamplerA, v_in.uv); + + // Create Mask + float4 mask = pMaskBase; + float4 imageB = pMaskInputB.Sample(maskSamplerB, v_in.uv); + mask += pMaskMatrix[0] * imageB.r; + mask += pMaskMatrix[1] * imageB.g; + mask += pMaskMatrix[2] * imageB.b; + mask += pMaskMatrix[3] * imageB.a; + mask *= pMaskMultiplier; + + return imageA * mask; +} + +technique Mask +{ + pass + { + vertex_shader = VSDefault(v_in); + pixel_shader = PSChannelMask(v_in); + } +} +// -------------------------------------------------------------------------------- // diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index e2526c2..5d9c9bb 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1,5 +1,9 @@ # Generic Advanced="Advanced Options" +Channel.Red="Red" +Channel.Green="Green" +Channel.Blue="Blue" +Channel.Alpha="Alpha" FileType.Image="Image" FileType.Images="Images" FileType.Video="Video" @@ -8,6 +12,8 @@ FileType.Sound="Sound" FileType.Sounds="Sounds" FileType.Effect="Effect" FileType.Effects="Effects" +SourceType.Source="Source" +SourceType.Scene="Scene" # Blur Blur.Type.Box="Box" @@ -102,6 +108,19 @@ Filter.Displacement.File.Types="Images (*.png *.jpeg *.jpg *.bmp *.tga);;All Fil Filter.Displacement.Ratio="Ratio" Filter.Displacement.Scale="Scale" +# Filter - Dynamic Mask +Filter.DynamicMask="Dynamic Mask" +Filter.DynamicMask.Input="Input Source" +Filter.DynamicMask.Input.Description="TODO" +Filter.DynamicMask.Channel="Channel" +Filter.DynamicMask.Channel.Description="TODO" +Filter.DynamicMask.Channel.Value="%s Channel Value" +Filter.DynamicMask.Channel.Value.Description="TODO" +Filter.DynamicMask.Channel.Multiplier="%s Channel Multiplier" +Filter.DynamicMask.Channel.Multiplier.Description="TODO" +Filter.DynamicMask.Channel.Input="%s Channel Value from %s Input Channel" +Filter.DynamicMask.Channel.Input.Description="TODO" + # Filter - SDF Effects Filter.SDFEffects="Signed Distance Field Effects" Filter.SDFEffects.Shadow.Inner="Inner Shadow" diff --git a/source/filters/filter-dynamic-mask.cpp b/source/filters/filter-dynamic-mask.cpp new file mode 100644 index 0000000..396d5ba --- /dev/null +++ b/source/filters/filter-dynamic-mask.cpp @@ -0,0 +1,506 @@ +/* + * Modern effects for a modern Streamer + * Copyright (C) 2019 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-dynamic-mask.hpp" +#include +#include "strings.hpp" + +// Filter to allow dynamic masking +// Allow any channel to affect any other channel +// +// Red/Green/Blue/Alpha Mask Input +// - Red Mask Output +// - Blue Mask Output +// - Green Mask Output +// - Alpha Mask Output + +#define SOURCE_NAME "Filter.DynamicMask" + +#define S_INPUT "Filter.DynamicMask.Input" +#define S_CHANNEL "Filter.DynamicMask.Channel" +#define S_CHANNEL_VALUE "Filter.DynamicMask.Channel.Value" +#define S_CHANNEL_MULTIPLIER "Filter.DynamicMask.Channel.Multiplier" +#define S_CHANNEL_INPUT "Filter.DynamicMask.Channel.Input" + +static std::pair channel_translations[] = { + {filter::dynamic_mask::channel::Red, S_CHANNEL_RED}, + {filter::dynamic_mask::channel::Green, S_CHANNEL_GREEN}, + {filter::dynamic_mask::channel::Blue, S_CHANNEL_BLUE}, + {filter::dynamic_mask::channel::Alpha, S_CHANNEL_ALPHA}, +}; + +INITIALIZER(FilterDynamicMaskInit) +{ + initializerFunctions.push_back([] { filter::dynamic_mask::dynamic_mask_factory::initialize(); }); + finalizerFunctions.push_back([] { filter::dynamic_mask::dynamic_mask_factory::finalize(); }); +} + +static std::shared_ptr factory_instance = nullptr; + +void filter::dynamic_mask::dynamic_mask_factory::initialize() +{ + factory_instance = std::make_shared(); +} + +void filter::dynamic_mask::dynamic_mask_factory::finalize() +{ + factory_instance.reset(); +} + +std::shared_ptr filter::dynamic_mask::dynamic_mask_factory::get() +{ + return factory_instance; +} + +filter::dynamic_mask::dynamic_mask_factory::dynamic_mask_factory() +{ + memset(&sourceInfo, 0, sizeof(obs_source_info)); + sourceInfo.id = "obs-stream-effects-filter-dynamic-mask"; + sourceInfo.type = OBS_SOURCE_TYPE_FILTER; + sourceInfo.output_flags = OBS_SOURCE_VIDEO; + + sourceInfo.get_name = [](void*) { return P_TRANSLATE(SOURCE_NAME); }; + + sourceInfo.create = [](obs_data_t* settings, obs_source_t* source) { + return reinterpret_cast(new filter::dynamic_mask::dynamic_mask_instance(settings, source)); + }; + sourceInfo.destroy = [](void* _ptr) { + delete reinterpret_cast(_ptr); + }; + + sourceInfo.get_defaults2 = [](void*, obs_data_t* settings) { + obs_data_set_default_int(settings, S_CHANNEL, static_cast(channel::Red)); + for (auto kv : channel_translations) { + obs_data_set_default_double(settings, (std::string(S_CHANNEL_VALUE) + "." + kv.second).c_str(), 1.0); + obs_data_set_default_double(settings, (std::string(S_CHANNEL_MULTIPLIER) + "." + kv.second).c_str(), 1.0); + for (auto kv2 : channel_translations) { + obs_data_set_default_double( + settings, (std::string(S_CHANNEL_INPUT) + "." + kv.second + "." + kv2.second).c_str(), 0.0); + } + } + }; + sourceInfo.get_properties2 = [](void* _ptr, void* _type_data_ptr) { + obs_properties_t* props = obs_properties_create_param(_type_data_ptr, nullptr); + reinterpret_cast(_ptr)->get_properties(props); + return props; + }; + sourceInfo.update = [](void* _ptr, obs_data_t* settings) { + reinterpret_cast(_ptr)->update(settings); + }; + sourceInfo.load = [](void* _ptr, obs_data_t* settings) { + reinterpret_cast(_ptr)->load(settings); + }; + sourceInfo.save = [](void* _ptr, obs_data_t* settings) { + reinterpret_cast(_ptr)->save(settings); + }; + + sourceInfo.video_tick = [](void* _ptr, float_t _seconds) { + reinterpret_cast(_ptr)->video_tick(_seconds); + }; + sourceInfo.video_render = [](void* _ptr, gs_effect_t* _effect) { + reinterpret_cast(_ptr)->video_render(_effect); + }; + + obs_register_source(&sourceInfo); +} + +filter::dynamic_mask::dynamic_mask_factory::~dynamic_mask_factory() {} + +filter::dynamic_mask::dynamic_mask_instance::dynamic_mask_instance(obs_data_t* data, obs_source_t* self) : self(self) +{ + this->update(data); + + this->filter_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + this->final_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); + + { + char* file = obs_module_file("effects/channel-mask.effect"); + try { + this->effect = std::make_shared(file); + } catch (std::exception ex) { + P_LOG_ERROR("Loading channel mask effect failed with error(s):\n%s", ex.what()); + } + assert(this->effect != nullptr); + bfree(file); + } +} + +filter::dynamic_mask::dynamic_mask_instance::~dynamic_mask_instance() {} + +uint32_t filter::dynamic_mask::dynamic_mask_instance::get_width() +{ + return 0; +} + +uint32_t filter::dynamic_mask::dynamic_mask_instance::get_height() +{ + return 0; +} + +void filter::dynamic_mask::dynamic_mask_instance::get_properties(obs_properties_t* properties) +{ + obs_property_t* p; + + this->translation_map.clear(); + + { + p = obs_properties_add_list(properties, S_INPUT, P_TRANSLATE(S_INPUT), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + obs_property_set_long_description(p, P_TRANSLATE(P_DESC(S_INPUT))); + obs_property_list_add_string(p, "", ""); + obs::source_tracker::get()->enumerate( + [&p](std::string name, obs_source_t*) { + std::stringstream sstr; + sstr << name << " (" << P_TRANSLATE(S_SOURCETYPE_SOURCE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_video_sources); + obs::source_tracker::get()->enumerate( + [&p](std::string name, obs_source_t*) { + std::stringstream sstr; + sstr << name << " (" << P_TRANSLATE(S_SOURCETYPE_SCENE) << ")"; + obs_property_list_add_string(p, sstr.str().c_str(), name.c_str()); + return false; + }, + obs::source_tracker::filter_scenes); + } + + { + p = obs_properties_add_list(properties, S_CHANNEL, P_TRANSLATE(S_CHANNEL), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p, P_TRANSLATE(P_DESC(S_CHANNEL))); + for (auto kv : channel_translations) { + obs_property_list_add_int(p, P_TRANSLATE(kv.second), static_cast(kv.first)); + } + obs_property_set_modified_callback2(p, modified, this); + + for (auto kv : channel_translations) { + std::string color = P_TRANSLATE(kv.second); + + { + std::string _chv = P_TRANSLATE(S_CHANNEL_VALUE); + std::vector _chv_data(_chv.size() * 2 + color.size() * 2, '\0'); + sprintf_s(_chv_data.data(), _chv_data.size(), _chv.c_str(), color.c_str()); + auto _chv_key = std::tuple{kv.first, channel::Invalid, std::string(S_CHANNEL_VALUE)}; + translation_map.insert({_chv_key, std::string(_chv_data.begin(), _chv_data.end())}); + auto chv = translation_map.find(_chv_key); + std::string chv_key = std::string(S_CHANNEL_VALUE) + "." + kv.second; + + p = obs_properties_add_float_slider(properties, chv_key.c_str(), chv->second.c_str(), -100.0, 100.0, + 0.01); + obs_property_set_long_description(p, P_TRANSLATE(P_DESC(S_CHANNEL_VALUE))); + + std::string _chm = P_TRANSLATE(S_CHANNEL_MULTIPLIER); + std::vector _chm_data(_chm.size() * 2 + color.size() * 2, '\0'); + sprintf_s(_chm_data.data(), _chm_data.size(), _chm.c_str(), color.c_str()); + auto _chm_key = std::tuple{kv.first, channel::Invalid, std::string(S_CHANNEL_MULTIPLIER)}; + translation_map.insert({_chm_key, std::string(_chm_data.begin(), _chm_data.end())}); + auto chm = translation_map.find(_chm_key); + std::string chm_key = std::string(S_CHANNEL_MULTIPLIER) + "." + kv.second; + + p = obs_properties_add_float_slider(properties, chm_key.c_str(), chm->second.c_str(), -100.0, 100.0, + 0.01); + obs_property_set_long_description(p, P_TRANSLATE(P_DESC(S_CHANNEL_MULTIPLIER))); + } + } + } + + { + for (auto kv1 : channel_translations) { + std::string color1 = P_TRANSLATE(kv1.second); + for (auto kv2 : channel_translations) { + std::string color2 = P_TRANSLATE(kv2.second); + + std::string _chm = P_TRANSLATE(S_CHANNEL_INPUT); + std::vector _chm_data(_chm.size() * 2 + color1.size() * 2 + color2.size() * 2, '\0'); + sprintf_s(_chm_data.data(), _chm_data.size(), _chm.c_str(), color1.c_str(), color2.c_str()); + auto _chm_key = std::tuple{kv1.first, kv2.first, std::string(S_CHANNEL_INPUT)}; + translation_map.insert({_chm_key, std::string(_chm_data.begin(), _chm_data.end())}); + auto chm = translation_map.find(_chm_key); + std::string chm_key = std::string(S_CHANNEL_INPUT) + "." + kv1.second + "." + kv2.second; + + p = obs_properties_add_float_slider(properties, chm_key.c_str(), chm->second.c_str(), -100.0, 100.0, + 0.01); + obs_property_set_long_description(p, P_TRANSLATE(P_DESC(S_CHANNEL_INPUT))); + } + } + } +} + +void filter::dynamic_mask::dynamic_mask_instance::update(obs_data_t* settings) +{ + // Update source. + try { + this->input = std::make_shared(obs_data_get_string(settings, S_INPUT)); + this->input_capture = std::make_shared(this->input, self); + this->input->events.rename += std::bind(&filter::dynamic_mask::dynamic_mask_instance::input_renamed, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + } catch (...) { + this->input.reset(); + this->input_capture.reset(); + this->input_texture.reset(); + } + + // Update data store + for (auto kv1 : channel_translations) { + auto found = this->channels.find(kv1.first); + if (found == this->channels.end()) { + this->channels.insert({kv1.first, channel_data()}); + found = this->channels.find(kv1.first); + if (found == this->channels.end()) { + assert(found != this->channels.end()); + throw std::exception("Unable to insert element into data store."); + } + } + + std::string chv_key = std::string(S_CHANNEL_VALUE) + "." + kv1.second; + found->second.value = static_cast(obs_data_get_double(settings, chv_key.c_str())); + this->precalc.base.ptr[static_cast(kv1.first)] = found->second.value; + + std::string chm_key = std::string(S_CHANNEL_MULTIPLIER) + "." + kv1.second; + found->second.scale = static_cast(obs_data_get_double(settings, chm_key.c_str())); + this->precalc.scale.ptr[static_cast(kv1.first)] = found->second.scale; + + vec4* ch = nullptr; + switch (kv1.first) { + case channel::Red: + ch = &this->precalc.matrix.x; + break; + case channel::Green: + ch = &this->precalc.matrix.y; + break; + case channel::Blue: + ch = &this->precalc.matrix.z; + break; + case channel::Alpha: + ch = &this->precalc.matrix.t; + break; + } + + for (auto kv2 : channel_translations) { + std::string ab_key = std::string(S_CHANNEL_INPUT) + "." + kv1.second + "." + kv2.second; + found->second.values.ptr[static_cast(kv2.first)] = + static_cast(obs_data_get_double(settings, ab_key.c_str())); + ch->ptr[static_cast(kv2.first)] = found->second.values.ptr[static_cast(kv2.first)]; + } + } +} + +void filter::dynamic_mask::dynamic_mask_instance::load(obs_data_t* settings) +{ + update(settings); +} + +void filter::dynamic_mask::dynamic_mask_instance::save(obs_data_t* settings) +{ + if (this->input) { + obs_data_set_string(settings, S_INPUT, obs_source_get_name(this->input->get())); + } + + for (auto kv1 : channel_translations) { + auto found = this->channels.find(kv1.first); + if (found == this->channels.end()) { + this->channels.insert({kv1.first, channel_data()}); + found = this->channels.find(kv1.first); + if (found == this->channels.end()) { + assert(found != this->channels.end()); + throw std::exception("Unable to insert element into data store."); + } + } + + std::string chv_key = std::string(S_CHANNEL_VALUE) + "." + kv1.second; + obs_data_set_double(settings, chv_key.c_str(), static_cast(found->second.value)); + + std::string chm_key = std::string(S_CHANNEL_MULTIPLIER) + "." + kv1.second; + obs_data_set_double(settings, chm_key.c_str(), static_cast(found->second.scale)); + + for (auto kv2 : channel_translations) { + std::string ab_key = std::string(S_CHANNEL_INPUT) + "." + kv1.second + "." + kv2.second; + obs_data_set_double(settings, ab_key.c_str(), + static_cast(found->second.values.ptr[static_cast(kv2.first)])); + } + } +} + +void filter::dynamic_mask::dynamic_mask_instance::input_renamed(obs::source*, std::string old_name, + std::string new_name) +{ + obs_data_t* settings = obs_source_get_settings(self); + obs_data_set_string(settings, S_INPUT, new_name.c_str()); + obs_source_update(self, settings); +} + +bool filter::dynamic_mask::dynamic_mask_instance::modified(void*, obs_properties_t* properties, obs_property_t*, + obs_data_t* settings) +{ + channel mask = static_cast(obs_data_get_int(settings, S_CHANNEL)); + + for (auto kv1 : channel_translations) { + std::string chv_key = std::string(S_CHANNEL_VALUE) + "." + kv1.second; + obs_property_set_visible(obs_properties_get(properties, chv_key.c_str()), (mask == kv1.first)); + std::string chm_key = std::string(S_CHANNEL_MULTIPLIER) + "." + kv1.second; + obs_property_set_visible(obs_properties_get(properties, chm_key.c_str()), (mask == kv1.first)); + + for (auto kv2 : channel_translations) { + std::string io_key = std::string(S_CHANNEL_INPUT) + "." + kv1.second + "." + kv2.second; + obs_property_set_visible(obs_properties_get(properties, io_key.c_str()), (mask == kv1.first)); + } + } + + return true; +} + +void filter::dynamic_mask::dynamic_mask_instance::video_tick(float) +{ + have_input_texture = false; + have_filter_texture = false; + have_final_texture = false; +} + +void filter::dynamic_mask::dynamic_mask_instance::video_render(gs_effect_t* in_effect) +{ + obs_source_t* parent = obs_filter_get_parent(this->self); + obs_source_t* target = obs_filter_get_target(this->self); + uint32_t width = obs_source_get_base_width(target); + uint32_t height = obs_source_get_base_height(target); + + if (!self || !parent || !target || !width || !height || !input || !input_capture || !effect) { + obs_source_skip_video_filter(self); + return; + } else if (!input->width() || !input->height()) { + obs_source_skip_video_filter(self); + return; + } + + gs_effect_t* default_effect = obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT); + + try { // Capture filter and input + if (!have_filter_texture) { + if (obs_source_process_filter_begin(this->self, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) { + auto op = this->filter_rt->render(width, height); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_KEEP, GS_KEEP, GS_KEEP); + gs_ortho(0, (float)width, 0, (float)height, -1., 1.); + + obs_source_process_filter_end(this->self, default_effect, width, height); + + gs_blend_state_pop(); + } else { + throw std::exception("Failed to render filter."); + } + + this->filter_texture = this->filter_rt->get_texture(); + this->have_filter_texture = true; + } + + if (!have_input_texture) { + this->input_texture = this->input_capture->render(input->width(), input->height()); + this->have_input_texture = true; + } + + // Draw source + if (!this->have_final_texture) { + { + auto op = this->final_rt->render(width, height); + + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_KEEP, GS_KEEP, GS_KEEP); + gs_ortho(0, (float)width, 0, (float)height, -1., 1.); + + this->effect->get_parameter("pMaskInputA").set_texture(this->filter_texture); + this->effect->get_parameter("pMaskInputB").set_texture(this->input_texture); + + this->effect->get_parameter("pMaskBase").set_float4(this->precalc.base); + this->effect->get_parameter("pMaskMatrix").set_matrix(this->precalc.matrix); + this->effect->get_parameter("pMaskMultiplier").set_float4(this->precalc.scale); + + while (gs_effect_loop(this->effect->get_object(), "Mask")) { + gs_draw_sprite(0, 0, width, height); + } + + gs_blend_state_pop(); + } + + this->final_texture = this->final_rt->get_texture(); + this->have_final_texture = true; + } + } catch (...) { + obs_source_skip_video_filter(this->self); + return; + } + + if (!have_filter_texture || !have_input_texture || !have_final_texture) { + obs_source_skip_video_filter(this->self); + return; + } + + // Draw source + { + // It is important that we do not modify the blend state here, as it is set correctly by OBS + gs_set_cull_mode(GS_NEITHER); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_depth_function(GS_ALWAYS); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS); + gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO); + + gs_effect_t* final_effect = in_effect ? in_effect : default_effect; + gs_eparam_t* param = gs_effect_get_param_by_name(final_effect, "image"); + if (!param) { + P_LOG_ERROR(" Failed to set image param.", obs_source_get_name(this->self)); + obs_source_skip_video_filter(this->self); + return; + } else { + gs_effect_set_texture(param, this->final_texture->get_object()); + } + while (gs_effect_loop(final_effect, "Draw")) { + gs_draw_sprite(0, 0, width, height); + } + } +} diff --git a/source/filters/filter-dynamic-mask.hpp b/source/filters/filter-dynamic-mask.hpp new file mode 100644 index 0000000..f3bcdb2 --- /dev/null +++ b/source/filters/filter-dynamic-mask.hpp @@ -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 +#include +#include "gfx/gfx-source-texture.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/obs-source-tracker.hpp" +#include "obs/obs-source.hpp" +#include "plugin.hpp" + +// OBS +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4201) +#endif +#include +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +namespace filter { + namespace dynamic_mask { + enum class channel : int8_t { Invalid = -1, Red, Green, Blue, Alpha }; + + class dynamic_mask_factory { + obs_source_info sourceInfo; + + public: // Singleton + static void initialize(); + static void finalize(); + static std::shared_ptr get(); + + public: + dynamic_mask_factory(); + ~dynamic_mask_factory(); + }; + + class dynamic_mask_instance { + obs_source_t* self; + + std::map, std::string> translation_map; + + std::shared_ptr effect; + + bool have_filter_texture; + std::shared_ptr filter_rt; + std::shared_ptr filter_texture; + + bool have_input_texture; + std::shared_ptr input; + std::shared_ptr input_capture; + std::shared_ptr input_texture; + + bool have_final_texture; + std::shared_ptr final_rt; + std::shared_ptr final_texture; + + struct channel_data { + float_t value = 0.0; + float_t scale = 1.0; + vec4 values = {0}; + }; + std::map channels; + + struct _precalc { + vec4 base; + vec4 scale; + matrix4 matrix; + } precalc; + + public: + dynamic_mask_instance(obs_data_t* data, obs_source_t* self); + ~dynamic_mask_instance(); + + uint32_t get_width(); + uint32_t get_height(); + + void get_properties(obs_properties_t* properties); + void update(obs_data_t* settings); + void load(obs_data_t* settings); + void save(obs_data_t* settings); + + void input_renamed(obs::source* src, std::string old_name, std::string new_name); + + static bool modified(void* self, obs_properties_t* properties, obs_property_t* property, + obs_data_t* settings); + + void video_tick(float _time); + void video_render(gs_effect_t* effect); + }; + } // namespace dynamic_mask +} // namespace filter diff --git a/source/strings.hpp b/source/strings.hpp index d5418e6..4052fd5 100644 --- a/source/strings.hpp +++ b/source/strings.hpp @@ -42,6 +42,9 @@ #define S_FILETYPE_EFFECT "FileType.Effect" #define S_FILETYPE_EFFECTS "FileType.Effects" +#define S_SOURCETYPE_SOURCE "SourceType.Source" +#define S_SOURCETYPE_SCENE "SourceType.Scene" + #define S_BLUR_TYPE_BOX "Blur.Type.Box" #define S_BLUR_TYPE_BOX_LINEAR "Blur.Type.BoxLinear" #define S_BLUR_TYPE_GAUSSIAN "Blur.Type.Gaussian" @@ -61,3 +64,10 @@ #define S_MIPGENERATOR_BICUBIC "MipGenerator.Bicubic" #define S_MIPGENERATOR_LANCZOS "MipGenerator.Lanczos" #define S_MIPGENERATOR_INTENSITY "MipGenerator.Intensity" + +#define S_CHANNEL_RED "Channel.Red" +#define S_CHANNEL_GREEN "Channel.Green" +#define S_CHANNEL_BLUE "Channel.Blue" +#define S_CHANNEL_ALPHA "Channel.Alpha" + +