obs-StreamFX/source/filter-blur.cpp
Michael Fabian 'Xaymar' Dirks 43136b00c4 filter-blur: Precalculate correct filter width for Gaussian
Gaussian blur used to have a hard edge which was caused by the filter width being wider than the actual sampling range, which is what is determined by the size option. In order to fix this, we have to figure out the proper filter width for a sampling range, so that we avoid any and all hard edges.

Additionally removes the old texture method entirely.

Fixes #44
2019-02-01 08:35:58 +01:00

1120 lines
40 KiB
C++

/*
* Modern effects for a modern Streamer
* Copyright (C) 2017-2018 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-blur.hpp"
#include <cfloat>
#include <cinttypes>
#include <cmath>
#include <map>
#include "obs-source-tracker.hpp"
#include "strings.hpp"
#include "util-math.hpp"
// OBS
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4201)
#endif
#include <callback/signal.h>
#include <graphics/graphics.h>
#include <graphics/matrix4.h>
#include <util/platform.h>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
// Translation Strings
#define SOURCE_NAME "Filter.Blur"
#define P_TYPE "Filter.Blur.Type"
#define P_TYPE_BOX "Filter.Blur.Type.Box"
#define P_TYPE_BOXLINEAR "Filter.Blur.Type.BoxLinear"
#define P_TYPE_GAUSSIAN "Filter.Blur.Type.Gaussian"
#define P_TYPE_GAUSSIANLINEAR "Filter.Blur.Type.GaussianLinear"
#define P_TYPE_BILATERAL "Filter.Blur.Type.Bilateral"
#define P_SIZE "Filter.Blur.Size"
#define P_BILATERAL_SMOOTHING "Filter.Blur.Bilateral.Smoothing"
#define P_BILATERAL_SHARPNESS "Filter.Blur.Bilateral.Sharpness"
#define P_DIRECTIONAL "Filter.Blur.Directional"
#define P_DIRECTIONAL_ANGLE "Filter.Blur.Directional.Angle"
#define P_STEPSCALE "Filter.Blur.StepScale"
#define P_STEPSCALE_X "Filter.Blur.StepScale.X"
#define P_STEPSCALE_Y "Filter.Blur.StepScale.Y"
#define P_MASK "Filter.Blur.Mask"
#define P_MASK_TYPE "Filter.Blur.Mask.Type"
#define P_MASK_TYPE_REGION "Filter.Blur.Mask.Type.Region"
#define P_MASK_TYPE_IMAGE "Filter.Blur.Mask.Type.Image"
#define P_MASK_TYPE_SOURCE "Filter.Blur.Mask.Type.Source"
#define P_MASK_REGION_LEFT "Filter.Blur.Mask.Region.Left"
#define P_MASK_REGION_RIGHT "Filter.Blur.Mask.Region.Right"
#define P_MASK_REGION_TOP "Filter.Blur.Mask.Region.Top"
#define P_MASK_REGION_BOTTOM "Filter.Blur.Mask.Region.Bottom"
#define P_MASK_REGION_FEATHER "Filter.Blur.Mask.Region.Feather"
#define P_MASK_REGION_FEATHER_SHIFT "Filter.Blur.Mask.Region.Feather.Shift"
#define P_MASK_REGION_INVERT "Filter.Blur.Mask.Region.Invert"
#define P_MASK_IMAGE "Filter.Blur.Mask.Image"
#define P_MASK_SOURCE "Filter.Blur.Mask.Source"
#define P_MASK_COLOR "Filter.Blur.Mask.Color"
#define P_MASK_ALPHA "Filter.Blur.Mask.Alpha"
#define P_MASK_MULTIPLIER "Filter.Blur.Mask.Multiplier"
#define P_COLORFORMAT "Filter.Blur.ColorFormat"
enum ColorFormat : uint64_t { // ToDo: Refactor into full class.
RGB,
YUV, // 701
};
static uint8_t const max_kernel_size = 25;
// Search density for proper gaussian curve size. Lower means more accurate, but takes more time to calculate.
static double_t const search_density = 1. / 5000.;
static double_t const search_threshold = 1. / 256.;
// Initializer & Finalizer
INITIALIZER(filterBlurFactoryInitializer)
{
initializerFunctions.push_back([] { filter::blur::blur_factory::initialize(); });
finalizerFunctions.push_back([] { filter::blur::blur_factory::finalize(); });
}
static std::shared_ptr<filter::blur::blur_factory> factory_instance = nullptr;
void filter::blur::blur_factory::initialize()
{
factory_instance = std::make_shared<filter::blur::blur_factory>();
}
void filter::blur::blur_factory::finalize()
{
factory_instance.reset();
}
std::shared_ptr<filter::blur::blur_factory> filter::blur::blur_factory::get()
{
return factory_instance;
}
filter::blur::blur_factory::blur_factory()
{
memset(&source_info, 0, sizeof(obs_source_info));
source_info.id = "obs-stream-effects-filter-blur";
source_info.type = OBS_SOURCE_TYPE_FILTER;
source_info.output_flags = OBS_SOURCE_VIDEO;
source_info.get_name = get_name;
source_info.get_defaults = get_defaults;
source_info.get_properties = get_properties;
source_info.create = create;
source_info.destroy = destroy;
source_info.update = update;
source_info.activate = activate;
source_info.deactivate = deactivate;
source_info.video_tick = video_tick;
source_info.video_render = video_render;
source_info.load = load;
obs_register_source(&source_info);
P_LOG_INFO("<filter-blur> Precalculating Gaussian Blur Kernel...");
this->gaussian_widths.resize(max_kernel_size + 1);
for (size_t w = 1.; w <= max_kernel_size; w++) {
for (double_t h = FLT_EPSILON; h <= w; h += search_density) {
if (util::math::gaussian<double_t>(w, h) > search_threshold) {
this->gaussian_widths[w] = h;
break;
}
}
}
}
filter::blur::blur_factory::~blur_factory() {}
void filter::blur::blur_factory::on_list_fill()
{
obs_enter_graphics();
{
char* file = obs_module_file("effects/blur.effect");
try {
blur_effect = std::make_shared<gs::effect>(file);
} catch (std::runtime_error ex) {
P_LOG_ERROR("<filter-blur> Loading effect '%s' failed with error(s): %s", file, ex.what());
}
bfree(file);
}
{
char* file = obs_module_file("effects/color-conversion.effect");
try {
color_converter_effect = std::make_shared<gs::effect>(file);
} catch (std::runtime_error ex) {
P_LOG_ERROR("<filter-blur> Loading effect '%s' failed with error(s): %s", file, ex.what());
}
bfree(file);
}
{
char* file = obs_module_file("effects/mask.effect");
try {
mask_effect = std::make_shared<gs::effect>(file);
} catch (std::runtime_error ex) {
P_LOG_ERROR("<filter-blur> Loading effect '%s' failed with error(s): %s", file, ex.what());
}
bfree(file);
}
generate_kernel_textures();
obs_leave_graphics();
}
void filter::blur::blur_factory::on_list_empty()
{
obs_enter_graphics();
blur_effect.reset();
color_converter_effect.reset();
mask_effect.reset();
obs_leave_graphics();
}
void filter::blur::blur_factory::generate_gaussian_kernels()
{
// 2D texture, horizontal is value, vertical is kernel size.
size_t size_power_of_two = size_t(pow(2, util::math::get_power_of_two_exponent_ceil(max_kernel_size)));
std::vector<float_t> math_data(size_power_of_two);
std::shared_ptr<std::vector<float_t>> kernel_data;
for (size_t width = 1; width <= max_kernel_size; width++) {
size_t v = (width - 1) * size_power_of_two;
kernel_data = std::make_shared<std::vector<float_t>>(size_power_of_two);
// Calculate and normalize
float_t sum = 0;
for (size_t p = 0; p <= width; p++) {
math_data[p] = float_t(Gaussian1D(double_t(p), double_t(gaussian_widths[width])));
sum += math_data[p] * (p > 0 ? 2 : 1);
}
// Normalize to Texture Buffer
double_t inverse_sum = 1.0 / sum;
for (size_t p = 0; p <= width; p++) {
kernel_data->at(p) = math_data[p] * inverse_sum;
}
gaussian_kernels.insert({uint8_t(width), kernel_data});
}
}
void filter::blur::blur_factory::generate_kernel_textures()
{
generate_gaussian_kernels();
}
void* filter::blur::blur_factory::create(obs_data_t* data, obs_source_t* parent)
{
if (get()->sources.empty()) {
get()->on_list_fill();
}
filter::blur::blur_instance* ptr = new filter::blur::blur_instance(data, parent);
get()->sources.push_back(ptr);
return ptr;
}
void filter::blur::blur_factory::destroy(void* inptr)
{
filter::blur::blur_instance* ptr = reinterpret_cast<filter::blur::blur_instance*>(inptr);
get()->sources.remove(ptr);
if (get()->sources.empty()) {
get()->on_list_empty();
}
delete ptr;
}
void filter::blur::blur_factory::get_defaults(obs_data_t* data)
{
obs_data_set_default_int(data, P_TYPE, filter::blur::type::Box);
obs_data_set_default_int(data, P_SIZE, 5);
// Bilateral Only
obs_data_set_default_double(data, P_BILATERAL_SMOOTHING, 50.0);
obs_data_set_default_double(data, P_BILATERAL_SHARPNESS, 90.0);
// Masking
obs_data_set_default_bool(data, P_MASK, false);
obs_data_set_default_int(data, P_MASK_TYPE, mask_type::Region);
obs_data_set_default_double(data, P_MASK_REGION_LEFT, 0.0);
obs_data_set_default_double(data, P_MASK_REGION_RIGHT, 0.0);
obs_data_set_default_double(data, P_MASK_REGION_TOP, 0.0);
obs_data_set_default_double(data, P_MASK_REGION_BOTTOM, 0.0);
obs_data_set_default_double(data, P_MASK_REGION_FEATHER, 0.0);
obs_data_set_default_double(data, P_MASK_REGION_FEATHER_SHIFT, 0.0);
obs_data_set_default_bool(data, P_MASK_REGION_INVERT, false);
char* default_file = obs_module_file("white.png");
obs_data_set_default_string(data, P_MASK_IMAGE, default_file);
bfree(default_file);
obs_data_set_default_string(data, P_MASK_SOURCE, "");
obs_data_set_default_int(data, P_MASK_COLOR, 0xFFFFFFFFull);
obs_data_set_default_double(data, P_MASK_MULTIPLIER, 1.0);
// Directional Blur
obs_data_set_default_bool(data, P_DIRECTIONAL, false);
obs_data_set_default_double(data, P_DIRECTIONAL_ANGLE, 0.0);
// Scaling
obs_data_set_default_bool(data, P_STEPSCALE, false);
obs_data_set_default_double(data, P_STEPSCALE_X, 100.0);
obs_data_set_default_double(data, P_STEPSCALE_Y, 100.0);
// Advanced
obs_data_set_default_bool(data, S_ADVANCED, false);
obs_data_set_default_int(data, P_COLORFORMAT, ColorFormat::RGB);
}
obs_properties_t* filter::blur::blur_factory::get_properties(void* inptr)
{
return reinterpret_cast<filter::blur::blur_instance*>(inptr)->get_properties();
}
void filter::blur::blur_factory::update(void* inptr, obs_data_t* settings)
{
reinterpret_cast<filter::blur::blur_instance*>(inptr)->update(settings);
}
void filter::blur::blur_factory::load(void* inptr, obs_data_t* settings)
{
reinterpret_cast<filter::blur::blur_instance*>(inptr)->update(settings);
}
const char* filter::blur::blur_factory::get_name(void*)
{
return P_TRANSLATE(SOURCE_NAME);
}
uint32_t filter::blur::blur_factory::get_width(void* inptr)
{
return reinterpret_cast<filter::blur::blur_instance*>(inptr)->get_width();
}
uint32_t filter::blur::blur_factory::get_height(void* inptr)
{
return reinterpret_cast<filter::blur::blur_instance*>(inptr)->get_height();
}
void filter::blur::blur_factory::activate(void* inptr)
{
reinterpret_cast<filter::blur::blur_instance*>(inptr)->activate();
}
void filter::blur::blur_factory::deactivate(void* inptr)
{
reinterpret_cast<filter::blur::blur_instance*>(inptr)->deactivate();
}
void filter::blur::blur_factory::video_tick(void* inptr, float delta)
{
reinterpret_cast<filter::blur::blur_instance*>(inptr)->video_tick(delta);
}
void filter::blur::blur_factory::video_render(void* inptr, gs_effect_t* effect)
{
reinterpret_cast<filter::blur::blur_instance*>(inptr)->video_render(effect);
}
std::shared_ptr<gs::effect> filter::blur::blur_factory::get_effect(filter::blur::type)
{
return blur_effect;
}
std::string filter::blur::blur_factory::get_technique(filter::blur::type type)
{
switch (type) {
case type::Box:
return "Box";
case type::Gaussian:
return "Gaussian";
case type::Bilateral:
return "Bilateral";
case type::BoxLinear:
return "BoxLinear";
case type::GaussianLinear:
return "GaussianLinear";
}
return "";
}
std::shared_ptr<gs::effect> filter::blur::blur_factory::get_color_converter_effect()
{
return color_converter_effect;
}
std::shared_ptr<gs::effect> filter::blur::blur_factory::get_mask_effect()
{
return mask_effect;
}
std::shared_ptr<std::vector<float_t>> filter::blur::blur_factory::get_gaussian_kernel(uint8_t size)
{
return gaussian_kernels.at(size);
}
filter::blur::blur_instance::blur_instance(obs_data_t* settings, obs_source_t* parent)
: m_self(parent), m_source_rendered(false), m_output_rendered(false)
{
m_self = parent;
// Create RenderTargets
try {
this->m_source_rt = std::make_shared<gs::rendertarget>(GS_RGBA, GS_ZS_NONE);
this->m_output_rt1 = std::make_shared<gs::rendertarget>(GS_RGBA, GS_ZS_NONE);
this->m_output_rt2 = std::make_shared<gs::rendertarget>(GS_RGBA, GS_ZS_NONE);
} catch (std::exception ex) {
P_LOG_ERROR("<filter-blur:%s> Failed to create rendertargets, error %s.", obs_source_get_name(m_self),
ex.what());
}
// Get initial Blur effect.
m_blur_effect = filter::blur::blur_factory::get()->get_effect(filter::blur::type::Box);
update(settings);
}
filter::blur::blur_instance::~blur_instance()
{
this->m_mask.source.source_texture.reset();
this->m_output_rt1.reset();
this->m_output_rt2.reset();
this->m_source_rt.reset();
this->m_blur_effect.reset();
this->m_output_texture.reset();
}
bool filter::blur::blur_instance::apply_shared_param(gs_texture_t* input, float texelX, float texelY)
{
bool result = true;
result = result && gs_set_param_texture(m_blur_effect->get_object(), "u_image", input);
vec2 imageSize;
vec2_set(&imageSize, (float)gs_texture_get_width(input), (float)gs_texture_get_height(input));
result = result && gs_set_param_float2(m_blur_effect->get_object(), "u_imageSize", &imageSize);
vec2 imageTexelDelta;
vec2_set(&imageTexelDelta, 1.0f, 1.0f);
vec2_div(&imageTexelDelta, &imageTexelDelta, &imageSize);
result = result && gs_set_param_float2(m_blur_effect->get_object(), "u_imageTexel", &imageTexelDelta);
vec2 texel;
vec2_set(&texel, texelX, texelY);
result = result && gs_set_param_float2(m_blur_effect->get_object(), "u_texelDelta", &texel);
result = result && gs_set_param_int(m_blur_effect->get_object(), "u_radius", (int)m_blur_size);
result = result && gs_set_param_int(m_blur_effect->get_object(), "u_diameter", (int)(1 + (m_blur_size * 2)));
return result;
}
bool filter::blur::blur_instance::apply_bilateral_param()
{
if (m_blur_type != type::Bilateral)
return false;
if (m_blur_effect->has_parameter("bilateralSmoothing")) {
m_blur_effect->get_parameter("bilateralSmoothing")
.set_float((float)(m_blur_bilateral_smoothing * (1 + m_blur_size * 2)));
}
if (m_blur_effect->has_parameter("bilateralSharpness")) {
m_blur_effect->get_parameter("bilateralSharpness").set_float((float)(1.0 - m_blur_bilateral_sharpness));
}
return true;
}
bool filter::blur::blur_instance::apply_gaussian_param(uint8_t width)
{
auto kernel = filter::blur::blur_factory::get()->get_gaussian_kernel(width);
if (m_blur_effect->has_parameter("kernel")) {
m_blur_effect->get_parameter("kernel").set_float_array(&(kernel->front()), kernel->size());
}
return true;
}
bool filter::blur::blur_instance::apply_mask_parameters(std::shared_ptr<gs::effect> effect,
gs_texture_t* original_texture, gs_texture_t* blurred_texture)
{
if (effect->has_parameter("image_orig")) {
effect->get_parameter("image_orig").set_texture(original_texture);
}
if (effect->has_parameter("image_blur")) {
effect->get_parameter("image_blur").set_texture(blurred_texture);
}
// Region
if (m_mask.type == mask_type::Region) {
if (effect->has_parameter("mask_region_left")) {
effect->get_parameter("mask_region_left").set_float(m_mask.region.left);
}
if (effect->has_parameter("mask_region_right")) {
effect->get_parameter("mask_region_right").set_float(m_mask.region.right);
}
if (effect->has_parameter("mask_region_top")) {
effect->get_parameter("mask_region_top").set_float(m_mask.region.top);
}
if (effect->has_parameter("mask_region_bottom")) {
effect->get_parameter("mask_region_bottom").set_float(m_mask.region.bottom);
}
if (effect->has_parameter("mask_region_feather")) {
effect->get_parameter("mask_region_feather").set_float(m_mask.region.feather);
}
if (effect->has_parameter("mask_region_feather_shift")) {
effect->get_parameter("mask_region_feather_shift").set_float(m_mask.region.feather_shift);
}
}
// Image
if (m_mask.type == mask_type::Image) {
if (effect->has_parameter("mask_image")) {
if (m_mask.image.texture) {
effect->get_parameter("mask_image").set_texture(m_mask.image.texture);
} else {
effect->get_parameter("mask_image").set_texture(nullptr);
}
}
}
// Source
if (m_mask.type == mask_type::Source) {
if (effect->has_parameter("mask_image")) {
if (m_mask.source.texture) {
effect->get_parameter("mask_image").set_texture(m_mask.source.texture);
} else {
effect->get_parameter("mask_image").set_texture(nullptr);
}
}
}
// Shared
if (effect->has_parameter("mask_color")) {
effect->get_parameter("mask_color").set_float4(m_mask.color.r, m_mask.color.g, m_mask.color.b, m_mask.color.a);
}
if (effect->has_parameter("mask_multiplier")) {
effect->get_parameter("mask_multiplier").set_float(m_mask.multiplier);
}
return true;
}
bool filter::blur::blur_instance::modified_properties(void*, obs_properties_t* props, obs_property*,
obs_data_t* settings)
{
// bilateral blur
bool show_bilateral = (obs_data_get_int(settings, P_TYPE) == type::Bilateral);
obs_property_set_visible(obs_properties_get(props, P_BILATERAL_SMOOTHING), show_bilateral);
obs_property_set_visible(obs_properties_get(props, P_BILATERAL_SHARPNESS), show_bilateral);
// region
bool show_mask = obs_data_get_bool(settings, P_MASK);
mask_type mtype = static_cast<mask_type>(obs_data_get_int(settings, P_MASK_TYPE));
bool show_region = (mtype == mask_type::Region) && show_mask;
bool show_image = (mtype == mask_type::Image) && show_mask;
bool show_source = (mtype == mask_type::Source) && show_mask;
obs_property_set_visible(obs_properties_get(props, P_MASK_TYPE), show_mask);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_LEFT), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_TOP), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_RIGHT), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_BOTTOM), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_FEATHER), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_FEATHER_SHIFT), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_REGION_INVERT), show_region);
obs_property_set_visible(obs_properties_get(props, P_MASK_IMAGE), show_image);
obs_property_set_visible(obs_properties_get(props, P_MASK_SOURCE), show_source);
obs_property_set_visible(obs_properties_get(props, P_MASK_COLOR), show_image || show_source);
obs_property_set_visible(obs_properties_get(props, P_MASK_ALPHA), show_image || show_source);
obs_property_set_visible(obs_properties_get(props, P_MASK_MULTIPLIER), show_image || show_source);
// Directional Blur
bool show_directional = obs_data_get_bool(settings, P_DIRECTIONAL);
obs_property_set_visible(obs_properties_get(props, P_DIRECTIONAL_ANGLE), show_directional);
// Scaling
bool show_scaling = obs_data_get_bool(settings, P_STEPSCALE);
obs_property_set_visible(obs_properties_get(props, P_STEPSCALE_X), show_scaling);
obs_property_set_visible(obs_properties_get(props, P_STEPSCALE_Y), show_scaling);
// advanced
bool showAdvanced = obs_data_get_bool(settings, S_ADVANCED);
obs_property_set_visible(obs_properties_get(props, P_COLORFORMAT), showAdvanced);
return true;
}
obs_properties_t* filter::blur::blur_instance::get_properties()
{
obs_properties_t* pr = obs_properties_create();
obs_property_t* p = NULL;
p = obs_properties_add_list(pr, P_TYPE, P_TRANSLATE(P_TYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_TYPE)));
obs_property_set_modified_callback2(p, modified_properties, this);
obs_property_list_add_int(p, P_TRANSLATE(P_TYPE_BOX), filter::blur::type::Box);
obs_property_list_add_int(p, P_TRANSLATE(P_TYPE_BOXLINEAR), filter::blur::type::BoxLinear);
obs_property_list_add_int(p, P_TRANSLATE(P_TYPE_GAUSSIAN), filter::blur::type::Gaussian);
obs_property_list_add_int(p, P_TRANSLATE(P_TYPE_GAUSSIANLINEAR), filter::blur::type::GaussianLinear);
obs_property_list_add_int(p, P_TRANSLATE(P_TYPE_BILATERAL), filter::blur::type::Bilateral);
p = obs_properties_add_int_slider(pr, P_SIZE, P_TRANSLATE(P_SIZE), 1, 25, 1);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_SIZE)));
//obs_property_set_modified_callback(p, modified_properties);
// bilateral Only
p = obs_properties_add_float_slider(pr, P_BILATERAL_SMOOTHING, P_TRANSLATE(P_BILATERAL_SMOOTHING), 0.0, 100.0,
0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_BILATERAL_SMOOTHING)));
p = obs_properties_add_float_slider(pr, P_BILATERAL_SHARPNESS, P_TRANSLATE(P_BILATERAL_SHARPNESS), 0.0, 100.0,
0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_BILATERAL_SHARPNESS)));
// Mask
p = obs_properties_add_bool(pr, P_MASK, P_TRANSLATE(P_MASK));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK)));
obs_property_set_modified_callback2(p, modified_properties, this);
p = obs_properties_add_list(pr, P_MASK_TYPE, P_TRANSLATE(P_MASK_TYPE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_TYPE)));
obs_property_set_modified_callback2(p, modified_properties, this);
obs_property_list_add_int(p, P_TRANSLATE(P_MASK_TYPE_REGION), mask_type::Region);
obs_property_list_add_int(p, P_TRANSLATE(P_MASK_TYPE_IMAGE), mask_type::Image);
obs_property_list_add_int(p, P_TRANSLATE(P_MASK_TYPE_SOURCE), mask_type::Source);
/// Region
p = obs_properties_add_float_slider(pr, P_MASK_REGION_LEFT, P_TRANSLATE(P_MASK_REGION_LEFT), 0.0, 100.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_LEFT)));
p = obs_properties_add_float_slider(pr, P_MASK_REGION_TOP, P_TRANSLATE(P_MASK_REGION_TOP), 0.0, 100.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_TOP)));
p = obs_properties_add_float_slider(pr, P_MASK_REGION_RIGHT, P_TRANSLATE(P_MASK_REGION_RIGHT), 0.0, 100.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_RIGHT)));
p = obs_properties_add_float_slider(pr, P_MASK_REGION_BOTTOM, P_TRANSLATE(P_MASK_REGION_BOTTOM), 0.0, 100.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_BOTTOM)));
p = obs_properties_add_float_slider(pr, P_MASK_REGION_FEATHER, P_TRANSLATE(P_MASK_REGION_FEATHER), 0.0, 50.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_FEATHER)));
p = obs_properties_add_float_slider(pr, P_MASK_REGION_FEATHER_SHIFT, P_TRANSLATE(P_MASK_REGION_FEATHER_SHIFT),
-100.0, 100.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_FEATHER_SHIFT)));
p = obs_properties_add_bool(pr, P_MASK_REGION_INVERT, P_TRANSLATE(P_MASK_REGION_INVERT));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_REGION_INVERT)));
/// Image
p = obs_properties_add_path(pr, P_MASK_IMAGE, P_TRANSLATE(P_MASK_IMAGE), OBS_PATH_FILE, P_TRANSLATE(""), nullptr);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_IMAGE)));
/// Source
p = obs_properties_add_list(pr, P_MASK_SOURCE, P_TRANSLATE(P_MASK_SOURCE), OBS_COMBO_TYPE_LIST,
OBS_COMBO_FORMAT_STRING);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_SOURCE)));
obs::source_tracker::get()->enumerate(
[this, &p](std::string name, obs_source_t* source) {
obs_property_list_add_string(p, std::string(name + " (Source)").c_str(), name.c_str());
return false;
},
obs::source_tracker::filter_video_sources);
obs::source_tracker::get()->enumerate(
[this, &p](std::string name, obs_source_t* source) {
obs_property_list_add_string(p, std::string(name + " (Scene)").c_str(), name.c_str());
return false;
},
obs::source_tracker::filter_scenes);
/// Shared
p = obs_properties_add_color(pr, P_MASK_COLOR, P_TRANSLATE(P_MASK_COLOR));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_COLOR)));
p = obs_properties_add_float_slider(pr, P_MASK_ALPHA, P_TRANSLATE(P_MASK_ALPHA), 0.0, 100.0, 0.1);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_ALPHA)));
p = obs_properties_add_float_slider(pr, P_MASK_MULTIPLIER, P_TRANSLATE(P_MASK_MULTIPLIER), 0.0, 10.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_MASK_MULTIPLIER)));
// Directional Blur
p = obs_properties_add_bool(pr, P_DIRECTIONAL, P_TRANSLATE(P_DIRECTIONAL));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_DIRECTIONAL)));
obs_property_set_modified_callback2(p, modified_properties, this);
p = obs_properties_add_float_slider(pr, P_DIRECTIONAL_ANGLE, P_TRANSLATE(P_DIRECTIONAL_ANGLE), -180.0, 180.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_DIRECTIONAL_ANGLE)));
// Scaling
p = obs_properties_add_bool(pr, P_STEPSCALE, P_TRANSLATE(P_STEPSCALE));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_STEPSCALE)));
obs_property_set_modified_callback2(p, modified_properties, this);
p = obs_properties_add_float_slider(pr, P_STEPSCALE_X, P_TRANSLATE(P_STEPSCALE_X), 0.0, 1000.0, 0.01);
p = obs_properties_add_float_slider(pr, P_STEPSCALE_Y, P_TRANSLATE(P_STEPSCALE_Y), 0.0, 1000.0, 0.01);
// advanced
p = obs_properties_add_bool(pr, S_ADVANCED, P_TRANSLATE(S_ADVANCED));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(S_ADVANCED)));
obs_property_set_modified_callback2(p, modified_properties, this);
p = obs_properties_add_list(pr, P_COLORFORMAT, P_TRANSLATE(P_COLORFORMAT), OBS_COMBO_TYPE_LIST,
OBS_COMBO_FORMAT_INT);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_COLORFORMAT)));
obs_property_list_add_int(p, "RGB", ColorFormat::RGB);
obs_property_list_add_int(p, "YUV", ColorFormat::YUV);
return pr;
}
void filter::blur::blur_instance::update(obs_data_t* settings)
{
m_blur_type = (blur::type)obs_data_get_int(settings, P_TYPE);
m_blur_effect = blur_factory::get()->get_effect(m_blur_type);
m_blur_technique = blur_factory::get()->get_technique(m_blur_type);
m_blur_size = (uint64_t)obs_data_get_int(settings, P_SIZE);
// bilateral blur
m_blur_bilateral_smoothing = obs_data_get_double(settings, P_BILATERAL_SMOOTHING) / 100.0;
m_blur_bilateral_sharpness = obs_data_get_double(settings, P_BILATERAL_SHARPNESS) / 100.0;
// region
m_mask.enabled = obs_data_get_bool(settings, P_MASK);
if (m_mask.enabled) {
m_mask.type = static_cast<mask_type>(obs_data_get_int(settings, P_MASK_TYPE));
switch (m_mask.type) {
case mask_type::Region:
m_mask.region.left = float_t(obs_data_get_double(settings, P_MASK_REGION_LEFT) / 100.0);
m_mask.region.top = float_t(obs_data_get_double(settings, P_MASK_REGION_TOP) / 100.0);
m_mask.region.right = 1.0f - float_t(obs_data_get_double(settings, P_MASK_REGION_RIGHT) / 100.0);
m_mask.region.bottom = 1.0f - float_t(obs_data_get_double(settings, P_MASK_REGION_BOTTOM) / 100.0);
m_mask.region.feather = float_t(obs_data_get_double(settings, P_MASK_REGION_FEATHER) / 100.0);
m_mask.region.feather_shift = float_t(obs_data_get_double(settings, P_MASK_REGION_FEATHER_SHIFT) / 100.0);
m_mask.region.invert = obs_data_get_bool(settings, P_MASK_REGION_INVERT);
break;
case mask_type::Image:
m_mask.image.path = obs_data_get_string(settings, P_MASK_IMAGE);
break;
case mask_type::Source:
m_mask.source.name = obs_data_get_string(settings, P_MASK_SOURCE);
break;
}
if ((m_mask.type == mask_type::Image) || (m_mask.type == mask_type::Source)) {
uint32_t color = static_cast<uint32_t>(obs_data_get_int(settings, P_MASK_COLOR));
m_mask.color.r = ((color >> 0) & 0xFF) / 255.0f;
m_mask.color.g = ((color >> 8) & 0xFF) / 255.0f;
m_mask.color.b = ((color >> 16) & 0xFF) / 255.0f;
m_mask.color.a = static_cast<float_t>(obs_data_get_double(settings, P_MASK_ALPHA));
m_mask.multiplier = float_t(obs_data_get_double(settings, P_MASK_MULTIPLIER));
}
}
// Directional Blur
this->m_blur_directional = obs_data_get_bool(settings, P_DIRECTIONAL);
this->m_blur_angle = obs_data_get_double(settings, P_DIRECTIONAL_ANGLE);
// Scaling
this->m_blur_step_scaling = obs_data_get_bool(settings, P_STEPSCALE);
this->m_blur_step_scale.first = obs_data_get_double(settings, P_STEPSCALE_X) / 100.0;
this->m_blur_step_scale.second = obs_data_get_double(settings, P_STEPSCALE_Y) / 100.0;
// advanced
if (obs_data_get_bool(settings, S_ADVANCED)) {
m_color_format = obs_data_get_int(settings, P_COLORFORMAT);
} else {
m_color_format = obs_data_get_default_int(settings, P_COLORFORMAT);
}
}
void filter::blur::blur_instance::load(obs_data_t* settings)
{
update(settings);
}
uint32_t filter::blur::blur_instance::get_width()
{
return uint32_t(0);
}
uint32_t filter::blur::blur_instance::get_height()
{
return uint32_t(0);
}
void filter::blur::blur_instance::activate() {}
void filter::blur::blur_instance::deactivate() {}
void filter::blur::blur_instance::video_tick(float)
{
// Load Mask
if (m_mask.type == mask_type::Image) {
if (m_mask.image.path_old != m_mask.image.path) {
try {
m_mask.image.texture = std::make_shared<gs::texture>(m_mask.image.path);
m_mask.image.path_old = m_mask.image.path;
} catch (...) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to load image '%s'.", obs_source_get_name(m_self),
m_mask.image.path.c_str());
}
}
} else if (m_mask.type == mask_type::Source) {
if (m_mask.source.name_old != m_mask.source.name) {
try {
m_mask.source.source_texture = std::make_shared<gfx::source_texture>(m_mask.source.name, m_self);
m_mask.source.is_scene = (obs_scene_from_source(m_mask.source.source_texture->get_object()) != nullptr);
m_mask.source.name_old = m_mask.source.name;
} catch (...) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to grab source '%s'.", obs_source_get_name(m_self),
m_mask.source.name.c_str());
}
}
}
m_source_rendered = false;
m_output_rendered = false;
}
void filter::blur::blur_instance::video_render(gs_effect_t* effect)
{
obs_source_t* parent = obs_filter_get_parent(this->m_self);
obs_source_t* target = obs_filter_get_target(this->m_self);
uint32_t baseW = obs_source_get_base_width(target);
uint32_t baseH = obs_source_get_base_height(target);
vec4 black;
bool failed = false;
std::shared_ptr<gs::effect> colorConversionEffect = blur_factory::get()->get_color_converter_effect();
gs_effect_t* defaultEffect = obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT);
vec4_set(&black, 0, 0, 0, 0);
// Verify that we can actually run first.
if (!target || !parent || !this->m_self) {
obs_source_skip_video_filter(this->m_self);
return;
}
if ((baseW == 0) || (baseH == 0)) {
obs_source_skip_video_filter(this->m_self);
return;
}
if (!this->m_output_rt1 || !this->m_output_rt2 || !this->m_blur_effect) {
obs_source_skip_video_filter(this->m_self);
return;
}
if (!m_source_rendered) {
// Source To Texture
{
gs_blend_state_push();
gs_reset_blend_state();
gs_enable_color(true, true, true, true);
gs_enable_blending(true);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_set_cull_mode(GS_NEITHER);
gs_depth_function(GS_ALWAYS);
gs_blend_function(GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS);
gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO);
try {
auto op = this->m_source_rt->render(baseW, baseH);
// Orthographic Camera and clear RenderTarget.
gs_ortho(0, (float)baseW, 0, (float)baseH, -1., 1.);
gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &black, 0, 0);
// Render
if (obs_source_process_filter_begin(this->m_self, GS_RGBA, OBS_NO_DIRECT_RENDERING)) {
obs_source_process_filter_end(this->m_self, defaultEffect, baseW, baseH);
} else {
throw std::runtime_error("Failed to render source");
}
} catch (std::exception ex) {
gs_blend_state_pop();
obs_source_skip_video_filter(this->m_self);
return;
}
gs_blend_state_pop();
if (!(m_source_texture = this->m_source_rt->get_texture())) {
obs_source_skip_video_filter(m_self);
return;
}
}
m_source_rendered = true;
}
if (!m_output_rendered) {
m_output_texture = m_source_texture;
// Color Conversion RGB-YUV
if ((m_color_format != ColorFormat::RGB) && colorConversionEffect) {
gs_blend_state_push();
gs_reset_blend_state();
gs_enable_color(true, true, true, true);
gs_enable_blending(false);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_set_cull_mode(GS_NEITHER);
gs_depth_function(GS_ALWAYS);
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS);
gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO);
try {
auto op = this->m_output_rt1->render(baseW, baseH);
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
if (colorConversionEffect->has_parameter("image")) {
colorConversionEffect->get_parameter("image").set_texture(m_source_texture->get_object());
}
while (gs_effect_loop(colorConversionEffect->get_object(), "RGBToYUV")) {
gs_draw_sprite(m_source_texture->get_object(), 0, baseW, baseH);
}
} catch (std::exception ex) {
gs_blend_state_pop();
obs_source_skip_video_filter(m_self);
return;
}
gs_blend_state_pop();
if (!(m_output_texture = this->m_output_rt1->get_texture())) {
obs_source_skip_video_filter(m_self);
return;
}
// Swap RTs for further rendering.
std::swap(this->m_output_rt1, this->m_output_rt2);
}
// Blur
{
gs_blend_state_push();
gs_reset_blend_state();
gs_enable_color(true, true, true, true);
gs_enable_blending(false);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_set_cull_mode(GS_NEITHER);
gs_depth_function(GS_ALWAYS);
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS);
gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO);
std::pair<float, float> kvs[] = {{1.0f / baseW, 0.0f}, {0.0f, 1.0f / baseH}};
// Directional Blur
if (this->m_blur_directional) {
// Directional Blur changes how
kvs[0].first = (1.0f / baseW);
kvs[0].second = (1.0f / baseH);
kvs[1].first = kvs[0].first;
kvs[1].second = kvs[0].second;
double_t rad = this->m_blur_angle * PI / 180.0;
double_t c0 = cos(rad);
double_t s0 = sin(rad);
kvs[0].first *= c0;
kvs[0].second *= s0;
if (!this->m_blur_step_scaling) {
kvs[1].first *= 0.0;
kvs[1].second *= 0.0;
} else {
kvs[1].first *= s0;
kvs[1].second *= c0;
}
}
// Apply scaling
if (this->m_blur_step_scaling) {
if (!this->m_blur_directional) {
kvs[0].first *= float_t(this->m_blur_step_scale.first);
kvs[0].second *= float_t(this->m_blur_step_scale.second);
kvs[1].first *= float_t(this->m_blur_step_scale.first);
kvs[1].second *= float_t(this->m_blur_step_scale.second);
} else {
// Directional Blur changes how scaling works as it rotates and needs to be relative to the axis of rotation.
kvs[0].first *= float_t(this->m_blur_step_scale.first);
kvs[0].second *= float_t(this->m_blur_step_scale.first);
kvs[1].first *= float_t(this->m_blur_step_scale.second);
kvs[1].second *= float_t(this->m_blur_step_scale.second);
}
}
try {
for (auto v : kvs) {
float xpel = std::get<0>(v);
float ypel = std::get<1>(v);
if ((abs(xpel) <= FLT_EPSILON) && (abs(ypel) <= FLT_EPSILON)) {
// Ignore passes that have a 0 texel modifier.
continue;
}
{
auto op = this->m_output_rt1->render(baseW, baseH);
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
apply_shared_param(m_output_texture->get_object(), xpel, ypel);
apply_gaussian_param(uint8_t(this->m_blur_size));
apply_bilateral_param();
// Render
while (gs_effect_loop(this->m_blur_effect->get_object(), this->m_blur_technique.c_str())) {
gs_draw_sprite(m_output_texture->get_object(), 0, baseW, baseH);
}
}
if (!(m_output_texture = this->m_output_rt1->get_texture())) {
throw("Failed to get blur texture.");
}
// Swap RTs for further rendering.
std::swap(this->m_output_rt1, this->m_output_rt2);
}
} catch (std::exception ex) {
gs_blend_state_pop();
obs_source_skip_video_filter(this->m_self);
return;
}
gs_blend_state_pop();
}
// Mask
if (m_mask.enabled) {
gs_blend_state_push();
gs_reset_blend_state();
gs_enable_color(true, true, true, true);
gs_enable_blending(false);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_set_cull_mode(GS_NEITHER);
gs_depth_function(GS_ALWAYS);
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS);
gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO);
std::string technique = "";
switch (this->m_mask.type) {
case Region:
if (this->m_mask.region.feather > 0.001) {
if (this->m_mask.region.invert) {
technique = "RegionFeatherInverted";
} else {
technique = "RegionFeather";
}
} else {
if (this->m_mask.region.invert) {
technique = "RegionInverted";
} else {
technique = "Region";
}
}
break;
case Image:
case Source:
technique = "Image";
break;
}
if (m_mask.source.source_texture) {
uint32_t source_width = obs_source_get_width(this->m_mask.source.source_texture->get_object());
uint32_t source_height = obs_source_get_height(this->m_mask.source.source_texture->get_object());
if (source_width == 0) {
source_width = baseW;
}
if (source_height == 0) {
source_height = baseH;
}
if (this->m_mask.source.is_scene) {
obs_video_info ovi;
if (obs_get_video_info(&ovi)) {
source_width = ovi.base_width;
source_height = ovi.base_height;
}
}
this->m_mask.source.texture = this->m_mask.source.source_texture->render(source_width, source_height);
}
std::shared_ptr<gs::effect> mask_effect = blur_factory::get()->get_mask_effect();
apply_mask_parameters(mask_effect, m_source_texture->get_object(), m_output_texture->get_object());
try {
auto op = this->m_output_rt1->render(baseW, baseH);
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
// Render
while (gs_effect_loop(mask_effect->get_object(), technique.c_str())) {
gs_draw_sprite(m_output_texture->get_object(), 0, baseW, baseH);
}
} catch (std::exception ex) {
gs_blend_state_pop();
obs_source_skip_video_filter(this->m_self);
return;
}
gs_blend_state_pop();
if (!(m_output_texture = this->m_output_rt1->get_texture())) {
obs_source_skip_video_filter(this->m_self);
return;
}
// Swap RTs for further rendering.
std::swap(this->m_output_rt1, this->m_output_rt2);
}
m_output_rendered = true;
}
// Color Conversion RGB-YUV or Straight Draw
{
// It is important that we do not modify the blend state here, as it is set correctly by OBS
gs_enable_color(true, true, true, true);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_set_cull_mode(GS_NEITHER);
gs_depth_function(GS_ALWAYS);
gs_stencil_function(GS_STENCIL_BOTH, GS_ALWAYS);
gs_stencil_op(GS_STENCIL_BOTH, GS_ZERO, GS_ZERO, GS_ZERO);
gs_effect_t* finalEffect = effect ? effect : defaultEffect;
const char* technique = "Draw";
if ((m_color_format == ColorFormat::YUV) && colorConversionEffect) {
finalEffect = colorConversionEffect->get_object();
technique = "YUVToRGB";
}
gs_eparam_t* param = gs_effect_get_param_by_name(finalEffect, "image");
if (!param) {
P_LOG_ERROR("<filter-blur:%s> Failed to set image param.", obs_source_get_name(this->m_self));
failed = true;
} else {
gs_effect_set_texture(param, m_output_texture->get_object());
}
while (gs_effect_loop(finalEffect, technique)) {
gs_draw_sprite(m_output_texture->get_object(), 0, baseW, baseH);
}
}
if (failed) {
obs_source_skip_video_filter(m_self);
return;
}
}