obs-StreamFX/source/filter-blur.cpp
Michael Fabian Dirks 9967a6ebf0 filter-blur: Add Color Format option
This option allows applying the blur to other color formats such as YUV. Bilateral Blur benefits the most from this, resulting in smoother images at lower kernel sizes.
2017-07-06 06:02:41 +02:00

649 lines
19 KiB
C++

/*
* Modern effects for a modern Streamer
* Copyright (C) 2017 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "filter-blur.h"
#include "strings.h"
extern "C" {
#pragma warning (push)
#pragma warning (disable: 4201)
#include "libobs/util/platform.h"
#include "libobs/graphics/graphics.h"
#include "libobs/graphics/matrix4.h"
#pragma warning (pop)
}
enum ColorFormat : uint64_t {
RGB,
YUV, // 701
};
static gs_effect_t *g_boxBlurEffect, *g_gaussianBlurEffect, *g_bilateralBlurEffect, *g_colorConversionEffect;
Filter::Blur::Blur() {
memset(&sourceInfo, 0, sizeof(obs_source_info));
sourceInfo.id = "obs-stream-effects-filter-blur";
sourceInfo.type = OBS_SOURCE_TYPE_FILTER;
sourceInfo.output_flags = OBS_SOURCE_VIDEO;
sourceInfo.get_name = get_name;
sourceInfo.get_defaults = get_defaults;
sourceInfo.get_properties = get_properties;
sourceInfo.create = create;
sourceInfo.destroy = destroy;
sourceInfo.update = update;
sourceInfo.activate = activate;
sourceInfo.deactivate = deactivate;
sourceInfo.show = show;
sourceInfo.hide = hide;
sourceInfo.video_tick = video_tick;
sourceInfo.video_render = video_render;
obs_enter_graphics();
{
char* loadError = nullptr;
char* file = obs_module_file("effects/box-blur.effect");
g_boxBlurEffect = gs_effect_create_from_file(file, &loadError);
bfree(file);
if (loadError != nullptr) {
PLOG_ERROR("<filter-blur> Loading box-blur effect failed with error(s): %s", loadError);
bfree(loadError);
} else if (!g_boxBlurEffect) {
PLOG_ERROR("<filter-blur> Loading box-blur effect failed with unspecified error.");
}
}
{
char* loadError = nullptr;
char* file = obs_module_file("effects/gaussian-blur.effect");
g_gaussianBlurEffect = gs_effect_create_from_file(file, &loadError);
bfree(file);
if (loadError != nullptr) {
PLOG_ERROR("<filter-blur> Loading gaussian blur effect failed with error(s): %s", loadError);
bfree(loadError);
} else if (!g_gaussianBlurEffect) {
PLOG_ERROR("<filter-blur> Loading gaussian blur effect failed with unspecified error.");
}
}
{
char* loadError = nullptr;
char* file = obs_module_file("effects/bilateral-blur.effect");
g_bilateralBlurEffect = gs_effect_create_from_file(file, &loadError);
bfree(file);
if (loadError != nullptr) {
PLOG_ERROR("<filter-blur> Loading bilateral blur effect failed with error(s): %s", loadError);
bfree(loadError);
} else if (!g_bilateralBlurEffect) {
PLOG_ERROR("<filter-blur> Loading bilateral blur effect failed with unspecified error.");
}
}
{
char* loadError = nullptr;
char* file = obs_module_file("effects/color-conversion.effect");
g_colorConversionEffect = gs_effect_create_from_file(file, &loadError);
bfree(file);
if (loadError != nullptr) {
PLOG_ERROR("<filter-blur> Loading color conversion effect failed with error(s): %s", loadError);
bfree(loadError);
} else if (!g_colorConversionEffect) {
PLOG_ERROR("<filter-blur> Loading color conversion effect failed with unspecified error.");
}
}
obs_leave_graphics();
if (g_boxBlurEffect && g_gaussianBlurEffect && g_bilateralBlurEffect && g_colorConversionEffect)
obs_register_source(&sourceInfo);
}
Filter::Blur::~Blur() {
obs_enter_graphics();
gs_effect_destroy(g_colorConversionEffect);
gs_effect_destroy(g_bilateralBlurEffect);
gs_effect_destroy(g_gaussianBlurEffect);
gs_effect_destroy(g_boxBlurEffect);
obs_leave_graphics();
}
const char * Filter::Blur::get_name(void *) {
return P_TRANSLATE(P_FILTER_BLUR);
}
void Filter::Blur::get_defaults(obs_data_t *data) {
obs_data_set_default_int(data, P_FILTER_BLUR_TYPE, Filter::Blur::Type::Box);
obs_data_set_default_int(data, P_FILTER_BLUR_SIZE, 5);
// Bilateral Only
obs_data_set_default_double(data, P_FILTER_BLUR_BILATERAL_SMOOTHING, 50.0);
obs_data_set_default_double(data, P_FILTER_BLUR_BILATERAL_SHARPNESS, 90.0);
// Advanced
obs_data_set_default_bool(data, P_FILTER_BLUR_ADVANCED, false);
obs_data_set_default_int(data, P_FILTER_BLUR_ADVANCED_COLORFORMAT, ColorFormat::RGB);
}
obs_properties_t * Filter::Blur::get_properties(void *) {
obs_properties_t *pr = obs_properties_create();
obs_property_t* p = NULL;
p = obs_properties_add_list(pr, P_FILTER_BLUR_TYPE, P_TRANSLATE(P_FILTER_BLUR_TYPE),
obs_combo_type::OBS_COMBO_TYPE_LIST, obs_combo_format::OBS_COMBO_FORMAT_INT);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_FILTER_BLUR_TYPE)));
obs_property_set_modified_callback(p, modified_properties);
obs_property_list_add_int(p, P_TRANSLATE(P_FILTER_BLUR_TYPE_BOX),
Filter::Blur::Type::Box);
obs_property_list_add_int(p, P_TRANSLATE(P_FILTER_BLUR_TYPE_GAUSSIAN),
Filter::Blur::Type::Gaussian);
obs_property_list_add_int(p, P_TRANSLATE(P_FILTER_BLUR_TYPE_BILATERAL),
Filter::Blur::Type::Bilateral);
p = obs_properties_add_int_slider(pr, P_FILTER_BLUR_SIZE,
P_TRANSLATE(P_FILTER_BLUR_SIZE), 1, 25, 1);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_FILTER_BLUR_SIZE)));
obs_property_set_modified_callback(p, modified_properties);
// Bilateral Only
p = obs_properties_add_float_slider(pr, P_FILTER_BLUR_BILATERAL_SMOOTHING,
P_TRANSLATE(P_FILTER_BLUR_BILATERAL_SMOOTHING), 0.01, 100.0, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_FILTER_BLUR_BILATERAL_SMOOTHING)));
p = obs_properties_add_float_slider(pr, P_FILTER_BLUR_BILATERAL_SHARPNESS,
P_TRANSLATE(P_FILTER_BLUR_BILATERAL_SHARPNESS), 0, 99.99, 0.01);
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_FILTER_BLUR_BILATERAL_SHARPNESS)));
// Advanced
p = obs_properties_add_bool(pr, P_FILTER_BLUR_ADVANCED, P_TRANSLATE(P_FILTER_BLUR_ADVANCED));
obs_property_set_long_description(p, P_TRANSLATE(P_DESC(P_FILTER_BLUR_ADVANCED)));
obs_property_set_modified_callback(p, modified_properties);
p = obs_properties_add_list(pr, P_FILTER_BLUR_ADVANCED_COLORFORMAT,
P_TRANSLATE(P_FILTER_BLUR_ADVANCED_COLORFORMAT),
obs_combo_type::OBS_COMBO_TYPE_LIST, obs_combo_format::OBS_COMBO_FORMAT_INT);
obs_property_set_long_description(p,
P_TRANSLATE(P_DESC(P_FILTER_BLUR_ADVANCED_COLORFORMAT)));
obs_property_list_add_int(p, "RGB",
ColorFormat::RGB);
obs_property_list_add_int(p, "YUV",
ColorFormat::YUV);
return pr;
}
bool Filter::Blur::modified_properties(obs_properties_t *pr, obs_property_t *, obs_data_t *d) {
bool showBilateral = false;
switch (obs_data_get_int(d, P_FILTER_BLUR_TYPE)) {
case Filter::Blur::Type::Box:
break;
case Filter::Blur::Type::Gaussian:
break;
case Filter::Blur::Type::Bilateral:
showBilateral = true;
break;
}
// Bilateral Blur
obs_property_set_visible(obs_properties_get(pr, P_FILTER_BLUR_BILATERAL_SMOOTHING),
showBilateral);
obs_property_set_visible(obs_properties_get(pr, P_FILTER_BLUR_BILATERAL_SHARPNESS),
showBilateral);
// Advanced
bool showAdvanced = false;
if (obs_data_get_bool(d, P_FILTER_BLUR_ADVANCED))
showAdvanced = true;
obs_property_set_visible(obs_properties_get(pr, P_FILTER_BLUR_ADVANCED_COLORFORMAT),
showAdvanced);
return true;
}
void * Filter::Blur::create(obs_data_t *data, obs_source_t *source) {
return new Instance(data, source);
}
void Filter::Blur::destroy(void *ptr) {
delete reinterpret_cast<Instance*>(ptr);
}
uint32_t Filter::Blur::get_width(void *ptr) {
return reinterpret_cast<Instance*>(ptr)->get_width();
}
uint32_t Filter::Blur::get_height(void *ptr) {
return reinterpret_cast<Instance*>(ptr)->get_height();
}
void Filter::Blur::update(void *ptr, obs_data_t *data) {
reinterpret_cast<Instance*>(ptr)->update(data);
}
void Filter::Blur::activate(void *ptr) {
reinterpret_cast<Instance*>(ptr)->activate();
}
void Filter::Blur::deactivate(void *ptr) {
reinterpret_cast<Instance*>(ptr)->deactivate();
}
void Filter::Blur::show(void *ptr) {
reinterpret_cast<Instance*>(ptr)->show();
}
void Filter::Blur::hide(void *ptr) {
reinterpret_cast<Instance*>(ptr)->hide();
}
void Filter::Blur::video_tick(void *ptr, float time) {
reinterpret_cast<Instance*>(ptr)->video_tick(time);
}
void Filter::Blur::video_render(void *ptr, gs_effect_t *effect) {
reinterpret_cast<Instance*>(ptr)->video_render(effect);
}
Filter::Blur::Instance::Instance(obs_data_t *data, obs_source_t *context) : m_source(context) {
obs_enter_graphics();
m_effect = g_boxBlurEffect;
m_primaryRT = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
m_secondaryRT = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
m_rtHorizontal = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
m_rtVertical = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
obs_leave_graphics();
update(data);
}
Filter::Blur::Instance::~Instance() {
obs_enter_graphics();
gs_texrender_destroy(m_primaryRT);
gs_texrender_destroy(m_secondaryRT);
gs_texrender_destroy(m_rtHorizontal);
gs_texrender_destroy(m_rtVertical);
obs_leave_graphics();
}
void Filter::Blur::Instance::update(obs_data_t *data) {
m_type = (Type)obs_data_get_int(data, P_FILTER_BLUR_TYPE);
switch (m_type) {
case Filter::Blur::Type::Box:
m_effect = g_boxBlurEffect;
break;
case Filter::Blur::Type::Gaussian:
m_effect = g_gaussianBlurEffect;
break;
case Filter::Blur::Type::Bilateral:
m_effect = g_bilateralBlurEffect;
break;
}
m_size = (uint64_t)obs_data_get_int(data, P_FILTER_BLUR_SIZE);
// Bilateral Blur
m_bilateralSmoothing = obs_data_get_double(data, P_FILTER_BLUR_BILATERAL_SMOOTHING) / 100.0;
m_bilateralSharpness = obs_data_get_double(data, P_FILTER_BLUR_BILATERAL_SHARPNESS) / 100.0;
// Advanced
m_colorFormat = obs_data_get_int(data, P_FILTER_BLUR_ADVANCED_COLORFORMAT);
}
uint32_t Filter::Blur::Instance::get_width() {
return 0;
}
uint32_t Filter::Blur::Instance::get_height() {
return 0;
}
void Filter::Blur::Instance::activate() {}
void Filter::Blur::Instance::deactivate() {}
void Filter::Blur::Instance::show() {}
void Filter::Blur::Instance::hide() {}
void Filter::Blur::Instance::video_tick(float) {}
void Filter::Blur::Instance::video_render(gs_effect_t *effect) {
bool failed = false;
obs_source_t
*parent = obs_filter_get_parent(m_source),
*target = obs_filter_get_target(m_source);
uint32_t
baseW = obs_source_get_base_width(target),
baseH = obs_source_get_base_height(target);
// Skip rendering if our target, parent or context is not valid.
if (!target || !parent || !m_source || !m_effect || !m_primaryRT || !m_technique
|| !baseW || !baseH) {
obs_source_skip_video_filter(m_source);
return;
}
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(m_primaryRT);
if (!gs_texrender_begin(m_primaryRT, baseW, baseH)) {
PLOG_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
vec4 black;
vec4_zero(&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_blend_function_separate(
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::GS_BLEND_ZERO,
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::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);
// 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 {
PLOG_ERROR("<filter-blur> Unable to render source.");
failed = true;
}
gs_texrender_end(m_primaryRT);
}
if (failed) {
obs_source_skip_video_filter(m_source);
return;
}
sourceTexture = gs_texrender_get_texture(m_primaryRT);
if (!sourceTexture) {
PLOG_ERROR("<filter-blur> Failed to get source texture.");
obs_source_skip_video_filter(m_source);
return;
}
#pragma endregion Source To Texture
// Conversion
if (m_colorFormat == ColorFormat::YUV) {
gs_texrender_reset(m_secondaryRT);
if (!gs_texrender_begin(m_secondaryRT, baseW, baseH)) {
PLOG_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
vec4 black;
vec4_zero(&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_blend_function_separate(
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::GS_BLEND_ZERO,
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::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_eparam_t* param = gs_effect_get_param_by_name(g_colorConversionEffect, "image");
if (!param) {
PLOG_ERROR("<filter-blur:Final> Failed to set image param.");
failed = true;
} else {
gs_effect_set_texture(param, sourceTexture);
}
while (gs_effect_loop(g_colorConversionEffect, "RGBToYUV")) {
gs_draw_sprite(sourceTexture, 0, baseW, baseH);
}
gs_texrender_end(m_secondaryRT);
}
if (failed) {
obs_source_skip_video_filter(m_source);
return;
}
sourceTexture = gs_texrender_get_texture(m_secondaryRT);
if (!sourceTexture) {
PLOG_ERROR("<filter-blur> Failed to get source texture.");
obs_source_skip_video_filter(m_source);
return;
}
}
gs_texture_t *blurred = blur_render(sourceTexture, baseW, baseH);
if (blurred == nullptr) {
obs_source_skip_video_filter(m_source);
return;
}
// Draw final effect
{
gs_effect_t* finalEffect = defaultEffect;
const char* technique = "Draw";
if (m_colorFormat == ColorFormat::YUV) {
finalEffect = g_colorConversionEffect;
technique = "YUVToRGB";
}
gs_eparam_t* param = gs_effect_get_param_by_name(finalEffect, "image");
if (!param) {
PLOG_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);
}
}
if (failed) {
obs_source_skip_video_filter(m_source);
return;
}
}
gs_texture_t* Filter::Blur::Instance::blur_render(gs_texture_t* input, size_t baseW, size_t baseH) {
bool failed = false;
gs_texture_t *intermediate;
#pragma region Horizontal Pass
gs_texrender_reset(m_rtHorizontal);
if (!gs_texrender_begin(m_rtHorizontal, baseW, baseH)) {
PLOG_ERROR("<filter-blur:Horizontal> Failed to initialize.");
return nullptr;
} else {
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
// Clear to Black
vec4 black;
vec4_zero(&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_blend_function_separate(
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::GS_BLEND_ZERO,
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::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);
// Prepare Effect
if (!apply_effect_param(input, (float)(1.0 / baseW), 0)) {
PLOG_ERROR("<filter-blur:Horizontal> Failed to set effect parameters.");
failed = true;
} else {
while (gs_effect_loop(m_effect, "Draw")) {
gs_draw_sprite(input, 0, baseW, baseH);
}
}
gs_texrender_end(m_rtHorizontal);
}
if (failed) {
return nullptr;
}
intermediate = gs_texrender_get_texture(m_rtHorizontal);
if (!intermediate) {
PLOG_ERROR("<filter-blur:Horizontal> Failed to get intermediate texture.");
return nullptr;
}
#pragma endregion Horizontal Pass
#pragma region Vertical Pass
gs_texrender_reset(m_rtVertical);
if (!gs_texrender_begin(m_rtVertical, baseW, baseH)) {
PLOG_ERROR("<filter-blur:Vertical> Failed to initialize.");
return nullptr;
} else {
gs_ortho(0, (float)baseW, 0, (float)baseH, -1, 1);
// Clear to Black
vec4 black;
vec4_zero(&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_blend_function_separate(
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::GS_BLEND_ZERO,
gs_blend_type::GS_BLEND_ONE,
gs_blend_type::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);
// Prepare Effect
if (!apply_effect_param(intermediate, 0, (float)(1.0 / baseH))) {
PLOG_ERROR("<filter-blur:Vertical> Failed to set effect parameters.");
failed = true;
} else {
while (gs_effect_loop(m_effect, "Draw")) {
gs_draw_sprite(intermediate, 0, baseW, baseH);
}
}
gs_texrender_end(m_rtVertical);
}
if (failed) {
return nullptr;
}
intermediate = gs_texrender_get_texture(m_rtVertical);
if (!intermediate) {
PLOG_ERROR("<filter-blur:Vertical> Failed to get intermediate texture.");
return nullptr;
}
#pragma endregion Vertical Pass
return intermediate;
}
bool Filter::Blur::Instance::apply_effect_param(gs_texture_t* texture, float uvTexelX, float uvTexelY) {
gs_eparam_t *param;
// UV Stepping
param = gs_effect_get_param_by_name(m_effect, "texel");
if (!param) {
PLOG_ERROR("<filter-blur> Failed to set texel param.");
return false;
} else {
PLOG_DEBUG("<filter-blur> Applying texel parameter.");
vec2 texel;
vec2_set(&texel, uvTexelX, uvTexelY);
gs_effect_set_vec2(param, &texel);
}
// Filter Width
param = gs_effect_get_param_by_name(m_effect, "widthHalf");
if (!param) {
PLOG_ERROR("<filter-blur> Failed to set widthHalf param.");
return false;
} else {
PLOG_DEBUG("<filter-blur> Applying widthHalf parameter.");
gs_effect_set_int(param, (int)m_size);
}
param = gs_effect_get_param_by_name(m_effect, "width");
if (!param) {
PLOG_ERROR("<filter-blur> Failed to set width param.");
return false;
} else {
PLOG_DEBUG("<filter-blur> Applying width parameter.");
gs_effect_set_int(param, (int)(1 + m_size * 2));
}
// Texture
param = gs_effect_get_param_by_name(m_effect, "image");
if (!param) {
PLOG_ERROR("<filter-blur> Failed to set image param.");
return false;
} else {
PLOG_DEBUG("<filter-blur> Applying image parameter.");
gs_effect_set_texture(param, texture);
}
// Bilateral Blur
if (m_type == Type::Bilateral) {
param = gs_effect_get_param_by_name(m_effect, "bilateralSmoothing");
if (!param) {
PLOG_ERROR("<filter-blur> Failed to set bilateralSmoothing param.");
return false;
} else {
PLOG_DEBUG("<filter-blur> Applying bilateralSmoothing parameter.");
gs_effect_set_float(param,
(float)(m_bilateralSmoothing * (1 + m_size * 2)));
}
param = gs_effect_get_param_by_name(m_effect, "bilateralSharpness");
if (!param) {
PLOG_ERROR("<filter-blur> Failed to set bilateralSmoothing param.");
return false;
} else {
PLOG_DEBUG("<filter-blur> Applying bilateralSharpness parameter.");
gs_effect_set_float(param, (float)(1.0 - m_bilateralSharpness));
}
}
return true;
}