obs-StreamFX/source/filter-blur.cpp
Michael Fabian 'Xaymar' Dirks 93df9b50b8 filter-blur: Add Linear Box Blur and combine all effects
Linear Box Blur abuses the fact that with Linear Sampling we can sample up to four adjacent texels at the same time and get a correct result for Box Blur back. Using this the total number of sample for Box Blur is reduced by n, making the total either n+1 (Even Radius) or n (Odd Radius).

Additionally all blur effect files have been merged into a single blur.effect file to reduce the time required to change a single parameter name. New blur effects should be added as a new technique instead of as a new effect file.

See Also: #21 Blur Quality
2018-12-22 22:08:55 +01:00

1086 lines
37 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.h"
#include <inttypes.h>
#include <map>
#include <math.h>
#include "strings.h"
#include "util-math.h"
extern "C" {
#pragma warning(push)
#pragma warning(disable : 4201)
#include "callback/signal.h"
#include "graphics/graphics.h"
#include "graphics/matrix4.h"
#include "util/platform.h"
#pragma warning(pop)
}
// 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_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_COLORFORMAT "Filter.Blur.ColorFormat"
#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"
// Initializer & Finalizer
INITIALIZER(filterBlurFactoryInitializer)
{
initializerFunctions.push_back([] { filter::blur::blur_factory::initialize(); });
finalizerFunctions.push_back([] { filter::blur::blur_factory::finalize(); });
}
enum ColorFormat : uint64_t { // ToDo: Refactor into full class.
RGB,
YUV, // 701
};
static uint8_t const max_kernel_size = 25;
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(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(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(blur_effect->get_object(), "u_imageTexel", &imageTexelDelta);
vec2 texel;
vec2_set(&texel, texelX, texelY);
result = result && gs_set_param_float2(blur_effect->get_object(), "u_texelDelta", &texel);
result = result && gs_set_param_int(blur_effect->get_object(), "u_radius", (int)size);
result = result && gs_set_param_int(blur_effect->get_object(), "u_diameter", (int)(1 + (size * 2)));
return result;
}
bool filter::blur::blur_instance::apply_bilateral_param()
{
if (type != type::Bilateral)
return false;
if (blur_effect->has_parameter("bilateralSmoothing")) {
blur_effect->get_parameter("bilateralSmoothing").set_float((float)(bilateral_smoothing * (1 + size * 2)));
}
if (blur_effect->has_parameter("bilateralSharpness")) {
blur_effect->get_parameter("bilateralSharpness").set_float((float)(1.0 - bilateral_sharpness));
}
return true;
}
bool filter::blur::blur_instance::apply_gaussian_param()
{
std::shared_ptr<gs::texture> kernel = filter::blur::blur_factory::get()->get_kernel(filter::blur::type::Gaussian);
if (blur_effect->has_parameter("kernel")) {
blur_effect->get_parameter("kernel").set_texture(kernel);
} else {
return false;
}
if (blur_effect->has_parameter("kernelTexel")) {
float_t wb = 1.0f / kernel->get_width();
float_t hb = 1.0f / kernel->get_height();
blur_effect->get_parameter("kernelTexel").set_float2(wb, hb);
}
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 (mask.type == mask_type::Region) {
if (effect->has_parameter("mask_region_left")) {
effect->get_parameter("mask_region_left").set_float(mask.region.left);
}
if (effect->has_parameter("mask_region_right")) {
effect->get_parameter("mask_region_right").set_float(mask.region.right);
}
if (effect->has_parameter("mask_region_top")) {
effect->get_parameter("mask_region_top").set_float(mask.region.top);
}
if (effect->has_parameter("mask_region_bottom")) {
effect->get_parameter("mask_region_bottom").set_float(mask.region.bottom);
}
if (effect->has_parameter("mask_region_feather")) {
effect->get_parameter("mask_region_feather").set_float(mask.region.feather);
}
if (effect->has_parameter("mask_region_feather_shift")) {
effect->get_parameter("mask_region_feather_shift").set_float(mask.region.feather_shift);
}
}
// Image
if (mask.type == mask_type::Image) {
if (effect->has_parameter("mask_image")) {
if (mask.image.texture) {
effect->get_parameter("mask_image").set_texture(mask.image.texture);
} else {
effect->get_parameter("mask_image").set_texture(nullptr);
}
}
}
// Source
if (mask.type == mask_type::Source) {
if (effect->has_parameter("mask_image")) {
if (mask.source.texture) {
effect->get_parameter("mask_image").set_texture(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(mask.color.r, mask.color.g, mask.color.b, mask.color.a);
}
if (effect->has_parameter("mask_multiplier")) {
effect->get_parameter("mask_multiplier").set_float(mask.multiplier);
}
return true;
}
bool filter::blur::blur_instance::modified_properties(void* ptr, obs_properties_t* props, obs_property* prop,
obs_data_t* settings)
{
bool showBilateral = (obs_data_get_int(settings, P_TYPE) == type::Bilateral);
prop;
ptr;
// bilateral blur
obs_property_set_visible(obs_properties_get(props, P_BILATERAL_SMOOTHING), showBilateral);
obs_property_set_visible(obs_properties_get(props, P_BILATERAL_SHARPNESS), showBilateral);
// 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);
// advanced
bool showAdvanced = obs_data_get_bool(settings, S_ADVANCED);
obs_property_set_visible(obs_properties_get(props, P_COLORFORMAT), showAdvanced);
return true;
}
filter::blur::blur_instance::blur_instance(obs_data_t* settings, obs_source_t* parent)
{
m_source = parent;
obs_enter_graphics();
blur_effect = filter::blur::blur_factory::get()->get_effect(filter::blur::type::Box);
primary_rendertarget = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
secondary_rendertarget = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
horizontal_rendertarget = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
vertical_rendertarget = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
obs_leave_graphics();
if (!primary_rendertarget) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to create primary rendertarget.",
obs_source_get_name(m_source));
}
if (!secondary_rendertarget) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to create secondary rendertarget.",
obs_source_get_name(m_source));
}
if (!horizontal_rendertarget) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to create horizontal rendertarget.",
obs_source_get_name(m_source));
}
if (!vertical_rendertarget) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to create vertical rendertarget.",
obs_source_get_name(m_source));
}
update(settings);
}
filter::blur::blur_instance::~blur_instance()
{
obs_enter_graphics();
gs_texrender_destroy(primary_rendertarget);
gs_texrender_destroy(secondary_rendertarget);
gs_texrender_destroy(horizontal_rendertarget);
gs_texrender_destroy(vertical_rendertarget);
obs_leave_graphics();
}
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_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(S_FILEFILTERS_IMAGES), 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_enum_sources(
[](void* ptr, obs_source_t* source) {
obs_property_t* p = reinterpret_cast<obs_property_t*>(ptr);
obs_property_list_add_string(p, std::string(std::string(obs_source_get_name(source)) + " (Source)").c_str(),
obs_source_get_name(source));
return true;
},
p);
blur_factory::get()->enum_scenes([this, p](obs_scene_t* scene) {
struct data {
blur_instance* self;
obs_property_t* prop;
std::string parent_name;
};
obs_source_t* scene_source = obs_scene_get_source(scene);
P_LOG_DEBUG("<filter-blur> Instance '%s' adding scene '%s'.", obs_source_get_name(m_source),
obs_source_get_name(scene_source));
obs_property_list_add_string(p,
std::string(std::string(obs_source_get_name(scene_source)) + " (Scene)").c_str(),
obs_source_get_name(scene_source));
return true;
});
/// 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)));
// 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)
{
type = (blur::type)obs_data_get_int(settings, P_TYPE);
blur_effect = blur_factory::get()->get_effect(type);
blur_technique = blur_factory::get()->get_technique(type);
size = (uint64_t)obs_data_get_int(settings, P_SIZE);
// bilateral blur
bilateral_smoothing = obs_data_get_double(settings, P_BILATERAL_SMOOTHING) / 100.0;
bilateral_sharpness = obs_data_get_double(settings, P_BILATERAL_SHARPNESS) / 100.0;
// region
mask.enabled = obs_data_get_bool(settings, P_MASK);
if (mask.enabled) {
mask.type = static_cast<mask_type>(obs_data_get_int(settings, P_MASK_TYPE));
switch (mask.type) {
case mask_type::Region:
mask.region.left = float_t(obs_data_get_double(settings, P_MASK_REGION_LEFT) / 100.0);
mask.region.top = float_t(obs_data_get_double(settings, P_MASK_REGION_TOP) / 100.0);
mask.region.right = 1.0f - float_t(obs_data_get_double(settings, P_MASK_REGION_RIGHT) / 100.0);
mask.region.bottom = 1.0f - float_t(obs_data_get_double(settings, P_MASK_REGION_BOTTOM) / 100.0);
mask.region.feather = float_t(obs_data_get_double(settings, P_MASK_REGION_FEATHER) / 100.0);
mask.region.feather_shift = float_t(obs_data_get_double(settings, P_MASK_REGION_FEATHER_SHIFT) / 100.0);
mask.region.invert = obs_data_get_bool(settings, P_MASK_REGION_INVERT);
break;
case mask_type::Image:
mask.image.path = obs_data_get_string(settings, P_MASK_IMAGE);
break;
case mask_type::Source:
mask.source.name = obs_data_get_string(settings, P_MASK_SOURCE);
break;
}
if ((mask.type == mask_type::Image) || (mask.type == mask_type::Source)) {
uint32_t color = static_cast<uint32_t>(obs_data_get_int(settings, P_MASK_COLOR));
mask.color.r = ((color >> 0) & 0xFF) / 255.0f;
mask.color.g = ((color >> 8) & 0xFF) / 255.0f;
mask.color.b = ((color >> 16) & 0xFF) / 255.0f;
mask.color.a = static_cast<float_t>(obs_data_get_double(settings, P_MASK_ALPHA));
mask.multiplier = float_t(obs_data_get_double(settings, P_MASK_MULTIPLIER));
}
}
// advanced
if (obs_data_get_bool(settings, S_ADVANCED)) {
color_format = obs_data_get_int(settings, P_COLORFORMAT);
} else {
color_format = obs_data_get_default_int(settings, P_COLORFORMAT);
}
}
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)
{
if (mask.type == mask_type::Image) {
if (mask.image.path_old != mask.image.path) {
try {
mask.image.texture = std::make_shared<gs::texture>(mask.image.path);
mask.image.path_old = mask.image.path;
} catch (...) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to load image '%s'.", obs_source_get_name(m_source),
mask.image.path.c_str());
}
}
} else if (mask.type == mask_type::Source) {
if (mask.source.name_old != mask.source.name) {
try {
mask.source.source_texture = std::make_shared<gfx::source_texture>(mask.source.name, m_source);
mask.source.is_scene = (obs_scene_from_source(mask.source.source_texture->get_object()) != nullptr);
mask.source.name_old = mask.source.name;
} catch (...) {
P_LOG_ERROR("<filter-blur> Instance '%s' failed to grab source '%s'.", obs_source_get_name(m_source),
mask.source.name.c_str());
}
}
}
}
void filter::blur::blur_instance::video_render(gs_effect_t* effect)
{
obs_source_t* parent = obs_filter_get_parent(m_source);
obs_source_t* target = obs_filter_get_target(m_source);
uint32_t baseW = obs_source_get_base_width(target);
uint32_t baseH = obs_source_get_base_height(target);
vec4 black = {0};
bool failed = false;
std::shared_ptr<gs::effect> colorConversionEffect = blur_factory::get()->get_color_converter_effect();
// Skip rendering if our target, parent or context is not valid.
if (!target || !parent || !m_source) {
obs_source_skip_video_filter(m_source);
return;
}
if ((baseW == 0) || (baseH == 0)) {
if (!have_logged_error)
P_LOG_ERROR("<filter-blur> Instance '%s' has invalid size source '%s'.", obs_source_get_name(m_source),
obs_source_get_name(target));
have_logged_error = true;
obs_source_skip_video_filter(m_source);
return;
}
if (!primary_rendertarget || !blur_effect) {
if (!have_logged_error)
P_LOG_ERROR("<filter-blur> Instance '%s' is unable to render.", obs_source_get_name(m_source),
obs_source_get_name(target));
have_logged_error = true;
obs_source_skip_video_filter(m_source);
return;
}
have_logged_error = false;
gs_effect_t* defaultEffect = obs_get_base_effect(obs_base_effect::OBS_EFFECT_DEFAULT);
gs_texture_t* sourceTexture = nullptr;
#pragma region Source To Texture
gs_texrender_reset(primary_rendertarget);
if (!gs_texrender_begin(primary_rendertarget, baseW, baseH)) {
P_LOG_ERROR("<filter-blur> Failed to set up base texture.");
obs_source_skip_video_filter(m_source);
return;
} else {
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
// Clear to Black
gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &black, 0, 0);
// Render
if (obs_source_process_filter_begin(m_source, GS_RGBA, OBS_NO_DIRECT_RENDERING)) {
obs_source_process_filter_end(m_source, effect ? effect : defaultEffect, baseW, baseH);
} else {
P_LOG_ERROR("<filter-blur> Unable to render source.");
failed = true;
}
gs_texrender_end(primary_rendertarget);
}
if (failed) {
obs_source_skip_video_filter(m_source);
return;
}
sourceTexture = gs_texrender_get_texture(primary_rendertarget);
if (!sourceTexture) {
P_LOG_ERROR("<filter-blur> Failed to get source texture.");
obs_source_skip_video_filter(m_source);
return;
}
#pragma endregion Source To Texture
// Conversion
#pragma region RGB->YUV
if ((color_format == ColorFormat::YUV) && colorConversionEffect) {
gs_texrender_reset(secondary_rendertarget);
if (!gs_texrender_begin(secondary_rendertarget, baseW, baseH)) {
P_LOG_ERROR("<filter-blur> Failed to set up base texture.");
obs_source_skip_video_filter(m_source);
return;
} else {
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
// Clear to Black
gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &black, 0, 0);
// Set up camera stuff
gs_set_cull_mode(GS_NEITHER);
gs_reset_blend_state();
gs_enable_blending(false);
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_enable_color(true, true, true, true);
if (colorConversionEffect->has_parameter("image")) {
colorConversionEffect->get_parameter("image").set_texture(sourceTexture);
}
while (gs_effect_loop(colorConversionEffect->get_object(), "RGBToYUV")) {
gs_draw_sprite(sourceTexture, 0, baseW, baseH);
}
gs_texrender_end(secondary_rendertarget);
}
if (failed) {
obs_source_skip_video_filter(m_source);
return;
}
sourceTexture = gs_texrender_get_texture(secondary_rendertarget);
if (!sourceTexture) {
P_LOG_ERROR("<filter-blur> Failed to get source texture.");
obs_source_skip_video_filter(m_source);
return;
}
}
#pragma endregion RGB->YUV
#pragma region blur
// Set up camera stuff
gs_set_cull_mode(GS_NEITHER);
gs_reset_blend_state();
gs_enable_blending(true);
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_enable_color(true, true, true, true);
gs_texture_t* blurred = nullptr;
gs_texture_t* intermediate = sourceTexture;
std::tuple<const char*, gs_texrender_t*, float, float> kvs[] = {
std::make_tuple("Horizontal", horizontal_rendertarget, 1.0f / baseW, 0.0f),
std::make_tuple("Vertical", vertical_rendertarget, 0.0f, 1.0f / baseH),
};
for (auto v : kvs) {
const char* name = std::get<0>(v);
gs_texrender_t* rt = std::get<1>(v);
float xpel = std::get<2>(v), ypel = std::get<3>(v);
if (!apply_shared_param(intermediate, xpel, ypel))
break;
switch (type) {
case Gaussian:
apply_gaussian_param();
break;
case Bilateral:
apply_bilateral_param();
break;
}
gs_texrender_reset(rt);
if (!gs_texrender_begin(rt, baseW, baseH)) {
P_LOG_ERROR("<filter-blur:%s> Failed to begin rendering.", name);
break;
}
// Camera
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &black, 0, 0);
// Render
while (gs_effect_loop(this->blur_effect->get_object(), this->blur_technique.c_str())) {
gs_draw_sprite(intermediate, 0, baseW, baseH);
}
gs_texrender_end(rt);
intermediate = gs_texrender_get_texture(rt);
if (!intermediate) {
P_LOG_ERROR("<filter-blur:%s> Failed to get intermediate texture.", name);
break;
}
blurred = intermediate;
}
if (blurred == nullptr) {
obs_source_skip_video_filter(m_source);
return;
}
#pragma endregion blur
#pragma region Mask
if (mask.enabled) {
std::string technique = "";
switch (mask.type) {
case Region:
if (mask.region.feather > 0.001) {
if (mask.region.invert) {
technique = "RegionFeatherInverted";
} else {
technique = "RegionFeather";
}
} else {
if (mask.region.invert) {
technique = "RegionInverted";
} else {
technique = "Region";
}
}
break;
case Image:
case Source:
technique = "Image";
break;
}
if (mask.source.source_texture) {
uint32_t source_width = obs_source_get_width(mask.source.source_texture->get_object());
uint32_t source_height = obs_source_get_height(mask.source.source_texture->get_object());
if (source_width == 0) {
source_width = baseW;
}
if (source_height == 0) {
source_height = baseH;
}
if (mask.source.is_scene) {
obs_video_info ovi;
if (obs_get_video_info(&ovi)) {
source_width = ovi.base_width;
source_height = ovi.base_height;
}
}
mask.source.texture = 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, sourceTexture, blurred);
gs_texrender_reset(horizontal_rendertarget);
if (gs_texrender_begin(horizontal_rendertarget, baseW, baseH)) {
// Camera
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH, &black, 0, 0);
// Render
while (gs_effect_loop(mask_effect->get_object(), technique.c_str())) {
gs_draw_sprite(blurred, 0, baseW, baseH);
}
gs_texrender_end(horizontal_rendertarget);
blurred = gs_texrender_get_texture(horizontal_rendertarget);
}
}
#pragma endregion
#pragma region YUV->RGB or straight draw
// Draw final effect
{
gs_effect_t* finalEffect = defaultEffect;
const char* technique = "Draw";
if ((color_format == ColorFormat::YUV) && colorConversionEffect) {
finalEffect = colorConversionEffect->get_object();
technique = "YUVToRGB";
}
// Set up camera stuff
gs_set_cull_mode(GS_NEITHER);
gs_reset_blend_state();
gs_enable_blending(true);
gs_blend_function(GS_BLEND_SRCALPHA, GS_BLEND_INVSRCALPHA);
gs_enable_depth_test(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_enable_color(true, true, true, true);
gs_eparam_t* param = gs_effect_get_param_by_name(finalEffect, "image");
if (!param) {
P_LOG_ERROR("<filter-blur:Final> Failed to set image param.");
failed = true;
} else {
gs_effect_set_texture(param, blurred);
}
while (gs_effect_loop(finalEffect, technique)) {
gs_draw_sprite(blurred, 0, baseW, baseH);
}
}
#pragma endregion YUV->RGB or straight draw
if (failed) {
obs_source_skip_video_filter(m_source);
return;
}
}
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;
obs_register_source(&source_info);
auto osi = obs_get_signal_handler();
signal_handler_connect(osi, "source_create", scene_create_handler, this);
signal_handler_connect(osi, "source_destroy", scene_destroy_handler, this);
}
filter::blur::blur_factory::~blur_factory()
{
auto osi = obs_get_signal_handler();
signal_handler_disconnect(osi, "source_create", scene_create_handler, this);
signal_handler_disconnect(osi, "source_destroy", scene_destroy_handler, this);
}
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();
kernels.clear();
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> texture_Data(size_power_of_two * size_power_of_two);
std::vector<float_t> math_data(size_power_of_two);
for (size_t width = 1; width <= max_kernel_size; width++) {
size_t v = (width - 1) * 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(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++) {
texture_Data[v + p] = float_t(math_data[p] * inverse_sum);
}
}
// Create Texture
try {
auto texture_buffer = reinterpret_cast<uint8_t*>(texture_Data.data());
auto unsafe_buffer = const_cast<const uint8_t**>(&texture_buffer);
kernels.insert_or_assign(filter::blur::type::Gaussian,
std::make_shared<gs::texture>(uint32_t(size_power_of_two), uint32_t(size_power_of_two),
GS_R32F, 1, unsafe_buffer, gs::texture::flags::None));
} catch (std::runtime_error ex) {
P_LOG_ERROR("<filter-blur> Failed to create gaussian kernel texture.");
}
}
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();
}
}
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);
// region
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.0f);
obs_data_set_default_double(data, P_MASK_REGION_RIGHT, 0.0f);
obs_data_set_default_double(data, P_MASK_REGION_TOP, 0.0f);
obs_data_set_default_double(data, P_MASK_REGION_BOTTOM, 0.0f);
obs_data_set_default_double(data, P_MASK_REGION_FEATHER, 0.0f);
obs_data_set_default_double(data, P_MASK_REGION_FEATHER_SHIFT, 0.0f);
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);
// 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);
}
const char* filter::blur::blur_factory::get_name(void* inptr)
{
inptr;
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);
}
void filter::blur::blur_factory::scene_create_handler(void* ptr, calldata_t* data)
{
filter::blur::blur_factory* self = reinterpret_cast<filter::blur::blur_factory*>(ptr);
obs_source_t* source = nullptr;
calldata_get_ptr(data, "source", &source);
obs_scene_t* scene = obs_scene_from_source(source);
if (scene) {
self->scenes.insert_or_assign(std::string(obs_source_get_name(source)), scene);
}
}
void filter::blur::blur_factory::scene_destroy_handler(void* ptr, calldata_t* data)
{
filter::blur::blur_factory* self = reinterpret_cast<filter::blur::blur_factory*>(ptr);
obs_source_t* source = nullptr;
calldata_get_ptr(data, "source", &source);
obs_scene_t* scene = obs_scene_from_source(source);
if (scene) {
self->scenes.erase(std::string(obs_source_get_name(source)));
}
}
std::shared_ptr<gs::effect> filter::blur::blur_factory::get_effect(filter::blur::type 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::BoxLinear:
return "BoxLinear";
case type::Gaussian:
return "Gaussian";
case type::Bilateral:
return "Bilateral";
}
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<gs::texture> filter::blur::blur_factory::get_kernel(filter::blur::type type)
{
return kernels.at(type);
}
obs_scene_t* filter::blur::blur_factory::get_scene(std::string name)
{
auto kv = scenes.find(name);
if (kv != scenes.end()) {
return kv->second;
}
return nullptr;
}
void filter::blur::blur_factory::enum_scenes(std::function<bool(obs_scene_t*)> fnc)
{
for (auto kv : scenes) {
if (!fnc(kv.second)) {
break;
}
}
}
static filter::blur::blur_factory* factory_instance = nullptr;
void filter::blur::blur_factory::initialize()
{
factory_instance = new filter::blur::blur_factory();
}
void filter::blur::blur_factory::finalize()
{
delete factory_instance;
}
filter::blur::blur_factory* filter::blur::blur_factory::get()
{
return factory_instance;
}