diff --git a/plugins/obs-filters/CMakeLists.txt b/plugins/obs-filters/CMakeLists.txt index b5ab13684..c3b1355ce 100644 --- a/plugins/obs-filters/CMakeLists.txt +++ b/plugins/obs-filters/CMakeLists.txt @@ -5,6 +5,7 @@ set(obs-filters_SOURCES color-filter.c async-delay-filter.c crop-filter.c + chroma-key-filter.c mask-filter.c) add_library(obs-filters MODULE diff --git a/plugins/obs-filters/chroma-key-filter.c b/plugins/obs-filters/chroma-key-filter.c new file mode 100644 index 000000000..f78898312 --- /dev/null +++ b/plugins/obs-filters/chroma-key-filter.c @@ -0,0 +1,288 @@ +#include +#include +#include +#include + +#define SETTING_OPACITY "opacity" +#define SETTING_CONTRAST "contrast" +#define SETTING_BRIGHTNESS "brightness" +#define SETTING_GAMMA "gamma" +#define SETTING_COLOR_TYPE "key_color_type" +#define SETTING_KEY_COLOR "key_color" +#define SETTING_SIMILARITY "similarity" +#define SETTING_SMOOTHNESS "smoothness" +#define SETTING_SPILL "spill" + +#define TEXT_OPACITY obs_module_text("Opacity") +#define TEXT_CONTRAST obs_module_text("Contrast") +#define TEXT_BRIGHTNESS obs_module_text("Brightness") +#define TEXT_GAMMA obs_module_text("Gamma") +#define TEXT_COLOR_TYPE obs_module_text("KeyColorType") +#define TEXT_KEY_COLOR obs_module_text("KeyColor") +#define TEXT_SIMILARITY obs_module_text("Similarity") +#define TEXT_SMOOTHNESS obs_module_text("Smoothness") +#define TEXT_SPILL obs_module_text("ColorSpillReduction") + +struct chroma_key_filter_data { + obs_source_t *context; + + gs_effect_t *effect; + + gs_eparam_t *color_param; + gs_eparam_t *contrast_param; + gs_eparam_t *brightness_param; + gs_eparam_t *gamma_param; + + gs_eparam_t *pixel_size_param; + gs_eparam_t *chroma_param; + gs_eparam_t *key_rgb_param; + gs_eparam_t *similarity_param; + gs_eparam_t *smoothness_param; + gs_eparam_t *spill_param; + + struct vec4 color; + float contrast; + float brightness; + float gamma; + + struct vec4 key_rgb; + struct vec2 chroma; + float similarity; + float smoothness; + float spill; +}; + +static const char *chroma_key_name(void) +{ + return obs_module_text("ChromaKeyFilter"); +} + +static const float yuv_mat[16] = {0.182586f, -0.100644f, 0.439216f, 0.0f, + 0.614231f, -0.338572f, -0.398942f, 0.0f, + 0.062007f, 0.439216f, -0.040274f, 0.0f, + 0.062745f, 0.501961f, 0.501961f, 1.0f}; + +static inline void color_settings_update( + struct chroma_key_filter_data *filter, obs_data_t *settings) +{ + uint32_t opacity = (uint32_t)obs_data_get_int(settings, + SETTING_OPACITY); + uint32_t color = 0xFFFFFF | (((opacity * 255) / 100) << 24); + double contrast = obs_data_get_double(settings, SETTING_CONTRAST); + double brightness = obs_data_get_double(settings, SETTING_BRIGHTNESS); + double gamma = obs_data_get_double(settings, SETTING_GAMMA); + + contrast = (contrast < 0.0) ? + (1.0 / (-contrast + 1.0)) : (contrast + 1.0); + + brightness *= 0.5; + + gamma = (gamma < 0.0) ? (-gamma + 1.0) : (1.0 / (gamma + 1.0)); + + filter->contrast = (float)contrast; + filter->brightness = (float)brightness; + filter->gamma = (float)gamma; + + vec4_from_rgba(&filter->color, color); +} + +static inline void chroma_settings_update( + struct chroma_key_filter_data *filter, obs_data_t *settings) +{ + int64_t similarity = obs_data_get_int(settings, SETTING_SIMILARITY); + int64_t smoothness = obs_data_get_int(settings, SETTING_SMOOTHNESS); + int64_t spill = obs_data_get_int(settings, SETTING_SPILL); + uint32_t key_color = (uint32_t)obs_data_get_int(settings, + SETTING_KEY_COLOR); + const char *key_type = obs_data_get_string(settings, + SETTING_COLOR_TYPE); + struct vec4 key_color_v4; + struct matrix4 yuv_mat_m4; + + if (strcmp(key_type, "green") == 0) + key_color = 0x00FF00; + else if (strcmp(key_type, "blue") == 0) + key_color = 0xFF9900; + else if (strcmp(key_type, "magenta") == 0) + key_color = 0xFF00FF; + + vec4_from_rgba(&filter->key_rgb, key_color | 0xFF000000); + + memcpy(&yuv_mat_m4, yuv_mat, sizeof(yuv_mat)); + vec4_transform(&key_color_v4, &filter->key_rgb, &yuv_mat_m4); + vec2_set(&filter->chroma, key_color_v4.y, key_color_v4.z); + + filter->similarity = (float)similarity / 1000.0f; + filter->smoothness = (float)smoothness / 1000.0f; + filter->spill = (float)spill / 1000.0f; +} + +static void chroma_key_update(void *data, obs_data_t *settings) +{ + struct chroma_key_filter_data *filter = data; + + color_settings_update(filter, settings); + chroma_settings_update(filter, settings); +} + +static void chroma_key_destroy(void *data) +{ + struct chroma_key_filter_data *filter = data; + + if (filter->effect) { + obs_enter_graphics(); + gs_effect_destroy(filter->effect); + obs_leave_graphics(); + } + + bfree(data); +} + +static void *chroma_key_create(obs_data_t *settings, obs_source_t *context) +{ + struct chroma_key_filter_data *filter = + bzalloc(sizeof(struct chroma_key_filter_data)); + char *effect_path = obs_module_file("chroma_key_filter.effect"); + + filter->context = context; + + obs_enter_graphics(); + + filter->effect = gs_effect_create_from_file(effect_path, NULL); + if (filter) { + filter->color_param = gs_effect_get_param_by_name( + filter->effect, "color"); + filter->contrast_param = gs_effect_get_param_by_name( + filter->effect, "contrast"); + filter->brightness_param = gs_effect_get_param_by_name( + filter->effect, "brightness"); + filter->gamma_param = gs_effect_get_param_by_name( + filter->effect, "gamma"); + filter->chroma_param = gs_effect_get_param_by_name( + filter->effect, "chroma_key"); + filter->key_rgb_param = gs_effect_get_param_by_name( + filter->effect, "key_rgb"); + filter->pixel_size_param = gs_effect_get_param_by_name( + filter->effect, "pixel_size"); + filter->similarity_param = gs_effect_get_param_by_name( + filter->effect, "similarity"); + filter->smoothness_param = gs_effect_get_param_by_name( + filter->effect, "smoothness"); + filter->spill_param = gs_effect_get_param_by_name( + filter->effect, "spill"); + } + + obs_leave_graphics(); + + bfree(effect_path); + + if (!filter->effect) { + chroma_key_destroy(filter); + return NULL; + } + + chroma_key_update(filter, settings); + return filter; +} + +static void chroma_key_render(void *data, gs_effect_t *effect) +{ + struct chroma_key_filter_data *filter = data; + obs_source_t *target = obs_filter_get_target(filter->context); + uint32_t width = obs_source_get_base_width(target); + uint32_t height = obs_source_get_base_height(target); + struct vec2 pixel_size; + + obs_source_process_filter_begin(filter->context, GS_RGBA, + OBS_ALLOW_DIRECT_RENDERING); + + vec2_set(&pixel_size, 1.0f / (float)width, 1.0f / (float)height); + + gs_effect_set_vec4(filter->color_param, &filter->color); + gs_effect_set_float(filter->contrast_param, filter->contrast); + gs_effect_set_float(filter->brightness_param, filter->brightness); + gs_effect_set_float(filter->gamma_param, filter->gamma); + gs_effect_set_vec2(filter->chroma_param, &filter->chroma); + gs_effect_set_vec4(filter->key_rgb_param, &filter->key_rgb); + gs_effect_set_vec2(filter->pixel_size_param, &pixel_size); + gs_effect_set_float(filter->similarity_param, filter->similarity); + gs_effect_set_float(filter->smoothness_param, filter->smoothness); + gs_effect_set_float(filter->spill_param, filter->spill); + + obs_source_process_filter_end(filter->context, filter->effect, 0, 0); + + UNUSED_PARAMETER(effect); +} + +static bool key_type_changed(obs_properties_t *props, obs_property_t *p, + obs_data_t *settings) +{ + const char *type = obs_data_get_string(settings, SETTING_COLOR_TYPE); + bool custom = strcmp(type, "custom") == 0; + + obs_property_set_visible(obs_properties_get(props, SETTING_KEY_COLOR), + custom); + + UNUSED_PARAMETER(p); + return true; +} + +static obs_properties_t *chroma_key_properties(void *data) +{ + obs_properties_t *props = obs_properties_create(); + + obs_property_t *p = obs_properties_add_list(props, + SETTING_COLOR_TYPE, TEXT_COLOR_TYPE, + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, obs_module_text("Green"), "green"); + obs_property_list_add_string(p, obs_module_text("Blue"), "blue"); + obs_property_list_add_string(p, obs_module_text("Magenta"), "magenta"); + obs_property_list_add_string(p, obs_module_text("Custom"), "custom"); + + obs_property_set_modified_callback(p, key_type_changed); + + obs_properties_add_color(props, SETTING_KEY_COLOR, TEXT_KEY_COLOR); + obs_properties_add_int_slider(props, SETTING_SIMILARITY, + TEXT_SIMILARITY, 1, 1000, 1); + obs_properties_add_int_slider(props, SETTING_SMOOTHNESS, + TEXT_SMOOTHNESS, 1, 1000, 1); + obs_properties_add_int_slider(props, SETTING_SPILL, + TEXT_SPILL, 1, 1000, 1); + + obs_properties_add_int(props, SETTING_OPACITY, TEXT_OPACITY, 0, 100, 1); + obs_properties_add_float_slider(props, SETTING_CONTRAST, + TEXT_CONTRAST, -1.0, 1.0, 0.01); + obs_properties_add_float_slider(props, SETTING_BRIGHTNESS, + TEXT_BRIGHTNESS, -1.0, 1.0, 0.01); + obs_properties_add_float_slider(props, SETTING_GAMMA, + TEXT_GAMMA, -1.0, 1.0, 0.01); + + UNUSED_PARAMETER(data); + return props; +} + +static void chroma_key_defaults(obs_data_t *settings) +{ + obs_data_set_default_int(settings, SETTING_OPACITY, 100); + obs_data_set_default_double(settings, SETTING_CONTRAST, 0.0); + obs_data_set_default_double(settings, SETTING_BRIGHTNESS, 0.0); + obs_data_set_default_double(settings, SETTING_GAMMA, 0.0); + obs_data_set_default_int(settings, SETTING_KEY_COLOR, 0x00FF00); + obs_data_set_default_string(settings, SETTING_COLOR_TYPE, "green"); + obs_data_set_default_int(settings, SETTING_SIMILARITY, 400); + obs_data_set_default_int(settings, SETTING_SMOOTHNESS, 80); + obs_data_set_default_int(settings, SETTING_SPILL, 100); +} + +struct obs_source_info chroma_key_filter = { + .id = "chroma_key_filter", + .type = OBS_SOURCE_TYPE_FILTER, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = chroma_key_name, + .create = chroma_key_create, + .destroy = chroma_key_destroy, + .video_render = chroma_key_render, + .update = chroma_key_update, + .get_properties = chroma_key_properties, + .get_defaults = chroma_key_defaults +}; diff --git a/plugins/obs-filters/data/chroma_key_filter.effect b/plugins/obs-filters/data/chroma_key_filter.effect new file mode 100644 index 000000000..79df92522 --- /dev/null +++ b/plugins/obs-filters/data/chroma_key_filter.effect @@ -0,0 +1,132 @@ +uniform float4x4 ViewProj; +uniform texture2d image; +uniform float4x4 color_matrix = {1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; +uniform float3 color_range_min = {0.0, 0.0, 0.0}; +uniform float3 color_range_max = {1.0, 1.0, 1.0}; + +uniform float4x4 yuv_mat = { 0.182586, 0.614231, 0.062007, 0.062745, + -0.100644, -0.338572, 0.439216, 0.501961, + 0.439216, -0.398942, -0.040274, 0.501961, + 0.000000, 0.000000, 0.000000, 1.000000}; + +uniform float4 color; +uniform float contrast; +uniform float brightness; +uniform float gamma; + +uniform float2 chroma_key; +uniform float4 key_rgb; +uniform float2 pixel_size; +uniform float similarity; +uniform float smoothness; +uniform float spill; + +sampler_state textureSampler { + Filter = Linear; + AddressU = Clamp; + AddressV = Clamp; +}; + +struct VertData { + float4 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +VertData VSDefault(VertData v_in) +{ + VertData vert_out; + vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); + vert_out.uv = v_in.uv; + return vert_out; +} + +float4 CalcColor(float4 rgba) +{ + return float4(pow(rgba.rgb, float3(gamma, gamma, gamma)) * contrast + brightness, rgba.a); +} + +float GetChromaDist(float3 rgb) +{ + float4 yuvx = mul(float4(rgb.rgb, 1.0), yuv_mat); + return distance(chroma_key, yuvx.yz); +} + +float4 SampleYUVToRGB(float2 uv) +{ + float4 yuv = image.Sample(textureSampler, uv); + yuv.xyz = clamp(yuv.xyz, color_range_min, color_range_max); + return saturate(mul(float4(yuv.xyz, 1.0), color_matrix)); +} + +float4 SampleTexture(float2 uv, bool use_matrix) +{ + if (use_matrix) { + return SampleYUVToRGB(uv); + } else { + return image.Sample(textureSampler, uv); + } +} + +float GetBoxFilteredChromaDist(float3 rgb, float2 texCoord, bool use_matrix) +{ + float distVal = GetChromaDist(rgb); + distVal += GetChromaDist(SampleTexture(texCoord-pixel_size, use_matrix).rgb); + distVal += GetChromaDist(SampleTexture(texCoord-float2(pixel_size.x, 0.0), use_matrix).rgb); + distVal += GetChromaDist(SampleTexture(texCoord-float2(pixel_size.x, -pixel_size.y), use_matrix).rgb); + + distVal += GetChromaDist(SampleTexture(texCoord-float2(0.0, pixel_size.y), use_matrix).rgb); + distVal += GetChromaDist(SampleTexture(texCoord+float2(0.0, pixel_size.y), use_matrix).rgb); + + distVal += GetChromaDist(SampleTexture(texCoord+float2(pixel_size.x, -pixel_size.y), use_matrix).rgb); + distVal += GetChromaDist(SampleTexture(texCoord+float2(pixel_size.x, 0.0), use_matrix).rgb); + distVal += GetChromaDist(SampleTexture(texCoord+pixel_size, use_matrix).rgb); + return distVal / 9.0; +} + +float4 ProcessChromaKey(float4 rgba, VertData v_in, bool use_matrix) +{ + float chromaDist = GetBoxFilteredChromaDist(rgba.rgb, v_in.uv, use_matrix); + float baseMask = chromaDist - similarity; + float fullMask = pow(saturate(baseMask / smoothness), 1.5); + float spillVal = pow(saturate(baseMask / spill), 1.5); + + rgba.a *= fullMask; + + float desat = (rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722); + rgba.rgb = saturate(float3(desat, desat, desat)) * (1.0 - spillVal) + rgba.rgb * spillVal; + + return CalcColor(rgba); +} + +float4 PSChromaKeyRGBA(VertData v_in) : TARGET +{ + float4 rgba = image.Sample(textureSampler, v_in.uv) * color; + return ProcessChromaKey(rgba, v_in, false); +} + +float4 PSChromaKeyMatrix(VertData v_in) : TARGET +{ + float4 rgba = SampleYUVToRGB(v_in.uv) * color; + return ProcessChromaKey(rgba, v_in, true); +} + +technique Draw +{ + pass + { + vertex_shader = VSDefault(v_in); + pixel_shader = PSChromaKeyRGBA(v_in); + } +} + +technique DrawMatrix +{ + pass + { + vertex_shader = VSDefault(v_in); + pixel_shader = PSChromaKeyMatrix(v_in); + } +} diff --git a/plugins/obs-filters/data/locale/en-US.ini b/plugins/obs-filters/data/locale/en-US.ini index 34be7917d..d9d7d0b85 100644 --- a/plugins/obs-filters/data/locale/en-US.ini +++ b/plugins/obs-filters/data/locale/en-US.ini @@ -2,6 +2,7 @@ ColorFilter="Color Correction" MaskFilter="Image Mask/Blend" AsyncDelayFilter="Video Delay (Async)" CropFilter="Crop" +ChromaKeyFilter="Chroma Key" DelayMs="Delay (milliseconds)" Type="Type" MaskBlendType.MaskColor="Alpha Mask (Color Channel)" @@ -17,6 +18,11 @@ Brightness="Brightness" Gamma="Gamma" BrowsePath.Images="All Image Files" BrowsePath.AllFiles="All Files" +KeyColorType="Key Color Type" +KeyColor="Key Color" +Similarity="Similarity (1-1000)" +Smoothness="Smoothness (1-1000)" +ColorSpillReduction="Key Color Spill Reduction (1-1000)" Crop.Left="Left" Crop.Right="Right" Crop.Top="Top" @@ -24,3 +30,7 @@ Crop.Bottom="Bottom" Crop.Width="Width" Crop.Height="Height" Crop.Relative="Relative" +CustomColor="Custom Color" +Green="Green" +Blue="Blue" +Magenta="Magenta" diff --git a/plugins/obs-filters/obs-filters.c b/plugins/obs-filters/obs-filters.c index 3ac7fe4f9..7f9d0c8c5 100644 --- a/plugins/obs-filters/obs-filters.c +++ b/plugins/obs-filters/obs-filters.c @@ -7,6 +7,7 @@ OBS_MODULE_USE_DEFAULT_LOCALE("obs-filters", "en-US") extern struct obs_source_info mask_filter; extern struct obs_source_info crop_filter; extern struct obs_source_info color_filter; +extern struct obs_source_info chroma_key_filter; extern struct obs_source_info async_delay_filter; bool obs_module_load(void) @@ -14,6 +15,7 @@ bool obs_module_load(void) obs_register_source(&mask_filter); obs_register_source(&crop_filter); obs_register_source(&color_filter); + obs_register_source(&chroma_key_filter); obs_register_source(&async_delay_filter); return true; }