From 506434c5e6386d298013aa1aecd8ce5f382c95be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Tue, 28 Jan 2020 10:09:51 +0100 Subject: [PATCH] obs-transitions: add track matte feature to the stinger transition This adds the ability to use a secondary black-and-white video as a mask between source A and B of the transition. The greyscale value of each pixel is used as the "slider" value in a linear interpolation between the corresponding pixels in source A and source B. The track matte can either be in the same file as the stinger itself (next to the stinger or under the stinger, doubling the width or height of the stinger depending of the selected layout) or a in a separate dedicated file. The same file/separate file behavior is controlled by the "Matte Layout" option in the stinger settings. --- plugins/obs-transitions/data/locale/en-US.ini | 8 + .../data/stinger_matte_transition.effect | 53 ++++ plugins/obs-transitions/transition-stinger.c | 280 +++++++++++++++++- 3 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 plugins/obs-transitions/data/stinger_matte_transition.effect diff --git a/plugins/obs-transitions/data/locale/en-US.ini b/plugins/obs-transitions/data/locale/en-US.ini index c3f58bcbb..6af835c83 100644 --- a/plugins/obs-transitions/data/locale/en-US.ini +++ b/plugins/obs-transitions/data/locale/en-US.ini @@ -15,8 +15,16 @@ VideoFile="Video File" TransitionPoint="Transition Point" TransitionPointFrame="Transition Point (frame)" TransitionPointType="Transition Point Type" +AudioTransitionPointType="Audio Transition Point Type" TransitionPointTypeFrame="Frame" TransitionPointTypeTime="Time (milliseconds)" +TrackMatteEnabled="Use a Track Matte" +InvertTrackMatte="Invert Matte Colors" +TrackMatteVideoFile="Track Matte Video File" +TrackMatteLayout="Matte Layout" +TrackMatteLayoutHorizontal="Same file, side-by-side (stinger on the left, track matte on the right)" +TrackMatteLayoutVertical="Same file, stacked (stinger on top, track matte at the bottom)" +TrackMatteLayoutSeparateFile="Separate file (warning: matte can get out of sync)" AudioFadeStyle="Audio Fade Style" AudioFadeStyle.FadeOutFadeIn="Fade out to transition point then fade in" AudioFadeStyle.CrossFade="Crossfade" diff --git a/plugins/obs-transitions/data/stinger_matte_transition.effect b/plugins/obs-transitions/data/stinger_matte_transition.effect new file mode 100644 index 000000000..15d847d81 --- /dev/null +++ b/plugins/obs-transitions/data/stinger_matte_transition.effect @@ -0,0 +1,53 @@ +uniform float4x4 ViewProj; +uniform texture2d a_tex; +uniform texture2d b_tex; +uniform texture2d matte_tex; +uniform bool invert_matte; + +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 PSStingerMatte(VertData v_in) : TARGET +{ + float2 uv = v_in.uv; + float4 a_color = a_tex.Sample(textureSampler, uv); + float4 b_color = b_tex.Sample(textureSampler, uv); + float4 matte_color = matte_tex.Sample(textureSampler, uv); + + // RGB -> Luma conversion using Rec. 709 factors + float matte_luma = ( + (matte_color.x * 0.2126) + + (matte_color.y * 0.7152) + + (matte_color.z * 0.0722) + ); + + // if matte invert is enabled, invert the matte color + matte_luma = (invert_matte ? (1.0 - matte_luma) : matte_luma); + + return lerp(a_color, b_color, matte_luma); +} + +technique StingerMatte +{ + pass + { + vertex_shader = VSDefault(v_in); + pixel_shader = PSStingerMatte(v_in); + } +} diff --git a/plugins/obs-transitions/transition-stinger.c b/plugins/obs-transitions/transition-stinger.c index a770f3e50..3cd1e0b27 100644 --- a/plugins/obs-transitions/transition-stinger.c +++ b/plugins/obs-transitions/transition-stinger.c @@ -4,12 +4,17 @@ #define TIMING_TIME 0 #define TIMING_FRAME 1 +#define MATTE_LAYOUT_HORIZONTAL 0 +#define MATTE_LAYOUT_VERTICAL 1 +#define MATTE_LAYOUT_SEPARATE_FILE 2 + enum fade_style { FADE_STYLE_FADE_OUT_FADE_IN, FADE_STYLE_CROSS_FADE }; struct stinger_info { obs_source_t *source; obs_source_t *media_source; + obs_source_t *matte_source; uint64_t duration_ns; uint64_t duration_frames; @@ -23,6 +28,20 @@ struct stinger_info { int monitoring_type; enum fade_style fade_style; + bool track_matte_enabled; + int matte_layout; + float matte_width_factor; + float matte_height_factor; + bool invert_matte; + + gs_effect_t *matte_effect; + gs_eparam_t *ep_a_tex; + gs_eparam_t *ep_b_tex; + gs_eparam_t *ep_matte_tex; + gs_eparam_t *ep_invert_matte; + + gs_texrender_t *matte_tex; + float (*mix_a)(void *data, float t); float (*mix_b)(void *data, float t); }; @@ -67,6 +86,36 @@ static void stinger_update(void *data, obs_data_t *settings) else s->transition_point_ns = (uint64_t)(point * 1000000LL); + s->track_matte_enabled = + obs_data_get_bool(settings, "track_matte_enabled"); + s->matte_layout = obs_data_get_int(settings, "track_matte_layout"); + s->matte_width_factor = + (s->matte_layout == MATTE_LAYOUT_HORIZONTAL ? 2.0f : 1.0f); + s->matte_height_factor = + (s->matte_layout == MATTE_LAYOUT_VERTICAL ? 2.0f : 1.0f); + s->invert_matte = obs_data_get_bool(settings, "invert_matte"); + + if (s->matte_source) { + obs_source_release(s->matte_source); + s->matte_source = NULL; + } + + if (s->track_matte_enabled && + s->matte_layout == MATTE_LAYOUT_SEPARATE_FILE) { + const char *tm_path = + obs_data_get_string(settings, "track_matte_path"); + + obs_data_t *tm_media_settings = obs_data_create(); + obs_data_set_string(tm_media_settings, "local_file", tm_path); + + s->matte_source = obs_source_create_private( + "ffmpeg_source", NULL, tm_media_settings); + obs_data_release(tm_media_settings); + + // no need to output sound from the matte video + obs_source_set_muted(s->matte_source, true); + } + s->monitoring_type = (int)obs_data_get_int(settings, "audio_monitoring"); obs_source_set_monitoring_type(s->media_source, s->monitoring_type); @@ -95,6 +144,33 @@ static void *stinger_create(obs_data_t *settings, obs_source_t *source) s->mix_a = mix_a_fade_in_out; s->mix_b = mix_b_fade_in_out; + char *effect_file = obs_module_file("stinger_matte_transition.effect"); + char *error_string = NULL; + obs_enter_graphics(); + s->matte_effect = + gs_effect_create_from_file(effect_file, &error_string); + obs_leave_graphics(); + + if (!s->matte_effect) { + blog(LOG_ERROR, + "Could not open stinger_matte_transition.effect: %s", + error_string); + bfree(error_string); + bfree(s); + return NULL; + } + + bfree(effect_file); + + s->ep_a_tex = gs_effect_get_param_by_name(s->matte_effect, "a_tex"); + s->ep_b_tex = gs_effect_get_param_by_name(s->matte_effect, "b_tex"); + s->ep_matte_tex = + gs_effect_get_param_by_name(s->matte_effect, "matte_tex"); + s->ep_invert_matte = + gs_effect_get_param_by_name(s->matte_effect, "invert_matte"); + + s->matte_tex = gs_texrender_create(GS_RGBA, GS_ZS_NONE); + obs_transition_enable_fixed(s->source, true, 0); obs_source_update(source, settings); return s; @@ -104,6 +180,12 @@ static void stinger_destroy(void *data) { struct stinger_info *s = data; obs_source_release(s->media_source); + obs_source_release(s->matte_source); + + gs_texrender_destroy(s->matte_tex); + + gs_effect_destroy(s->matte_effect); + bfree(s); } @@ -112,31 +194,92 @@ static void stinger_defaults(obs_data_t *settings) obs_data_set_default_bool(settings, "hw_decode", true); } +static void stinger_matte_render(void *data, gs_texture_t *a, gs_texture_t *b, + float t, uint32_t cx, uint32_t cy) +{ + struct stinger_info *s = data; + + struct vec4 background; + vec4_zero(&background); + + obs_source_t *matte_source = + (s->matte_layout == MATTE_LAYOUT_SEPARATE_FILE + ? s->matte_source + : s->media_source); + + float matte_cx = (float)obs_source_get_width(matte_source) / + s->matte_width_factor; + float matte_cy = (float)obs_source_get_height(matte_source) / + s->matte_height_factor; + + float width_offset = (s->matte_layout == MATTE_LAYOUT_HORIZONTAL + ? (-matte_cx) + : 0.0f); + float height_offset = + (s->matte_layout == MATTE_LAYOUT_VERTICAL ? (-matte_cy) : 0.0f); + + // Track matte media render + gs_texrender_reset(s->matte_tex); + if (matte_cx > 0 && matte_cy > 0) { + float scale_x = (float)cx / matte_cx; + float scale_y = (float)cy / matte_cy; + + if (gs_texrender_begin(s->matte_tex, cx, cy)) { + gs_matrix_push(); + gs_matrix_scale3f(scale_x, scale_y, 1.0f); + gs_matrix_translate3f(width_offset, height_offset, + 0.0f); + gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); + obs_source_video_render(matte_source); + gs_matrix_pop(); + + gs_texrender_end(s->matte_tex); + } + } + + gs_effect_set_texture(s->ep_a_tex, a); + gs_effect_set_texture(s->ep_b_tex, b); + gs_effect_set_texture(s->ep_matte_tex, + gs_texrender_get_texture(s->matte_tex)); + gs_effect_set_bool(s->ep_invert_matte, s->invert_matte); + + while (gs_effect_loop(s->matte_effect, "StingerMatte")) + gs_draw_sprite(NULL, 0, cx, cy); + + UNUSED_PARAMETER(t); +} + static void stinger_video_render(void *data, gs_effect_t *effect) { struct stinger_info *s = data; - float t = obs_transition_get_time(s->source); - bool use_a = t < s->transition_point; + if (s->track_matte_enabled) { + obs_transition_video_render(s->source, stinger_matte_render); + } else { + float t = obs_transition_get_time(s->source); + bool use_a = t < s->transition_point; - enum obs_transition_target target = use_a ? OBS_TRANSITION_SOURCE_A - : OBS_TRANSITION_SOURCE_B; + enum obs_transition_target target = + use_a ? OBS_TRANSITION_SOURCE_A + : OBS_TRANSITION_SOURCE_B; - if (!obs_transition_video_render_direct(s->source, target)) - return; + if (!obs_transition_video_render_direct(s->source, target)) + return; + } /* --------------------- */ float source_cx = (float)obs_source_get_width(s->source); float source_cy = (float)obs_source_get_height(s->source); + uint32_t media_cx = obs_source_get_width(s->media_source); uint32_t media_cy = obs_source_get_height(s->media_source); if (!media_cx || !media_cy) return; - float scale_x = source_cx / (float)media_cx; - float scale_y = source_cy / (float)media_cy; + float scale_x = source_cx / ((float)media_cx / s->matte_width_factor); + float scale_y = source_cy / ((float)media_cy / s->matte_height_factor); gs_matrix_push(); gs_matrix_scale3f(scale_x, scale_y, 1.0f); @@ -184,6 +327,10 @@ static bool stinger_audio_render(void *data, uint64_t *ts_out, struct stinger_info *s = data; uint64_t ts = 0; + if (!s) { + return false; + } + if (!obs_source_audio_pending(s->media_source)) { ts = obs_source_get_audio_timestamp(s->media_source); if (!ts) @@ -229,9 +376,14 @@ static void stinger_transition_start(void *data) proc_handler_t *ph = obs_source_get_proc_handler(s->media_source); + proc_handler_t *matte_ph = + obs_source_get_proc_handler(s->matte_source); if (s->transitioning) { proc_handler_call(ph, "restart", &cd); + if (matte_ph) { + proc_handler_call(matte_ph, "restart", &cd); + } return; } @@ -258,6 +410,18 @@ static void stinger_transition_start(void *data) s->transition_a_mul = (1.0f / s->transition_point); s->transition_b_mul = (1.0f / (1.0f - s->transition_point)); + if (s->track_matte_enabled) { + proc_handler_call(matte_ph, "get_duration", &cd); + uint64_t tm_duration_ns = + (uint64_t)calldata_int(&cd, "duration"); + + s->duration_ns = ((tm_duration_ns > s->duration_ns) + ? (tm_duration_ns) + : (s->duration_ns)); + + obs_source_add_active_child(s->source, s->matte_source); + } + obs_transition_enable_fixed( s->source, true, (uint32_t)(s->duration_ns / 1000000)); @@ -276,6 +440,9 @@ static void stinger_transition_stop(void *data) if (s->media_source) obs_source_remove_active_child(s->source, s->media_source); + if (s->matte_source) + obs_source_remove_active_child(s->source, s->matte_source); + s->transitioning = false; } @@ -286,6 +453,9 @@ static void stinger_enum_active_sources(void *data, struct stinger_info *s = data; if (s->media_source && s->transitioning) enum_callback(s->source, s->media_source, param); + + if (s->matte_source && s->transitioning) + enum_callback(s->source, s->matte_source, param); } static void stinger_enum_all_sources(void *data, @@ -295,6 +465,9 @@ static void stinger_enum_all_sources(void *data, struct stinger_info *s = data; if (s->media_source) enum_callback(s->source, s->media_source, param); + + if (s->matte_source) + enum_callback(s->source, s->matte_source, param); } #define FILE_FILTER \ @@ -304,17 +477,56 @@ static bool transition_point_type_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *s) { int64_t type = obs_data_get_int(s, "tp_type"); - p = obs_properties_get(ppts, "transition_point"); + + obs_property_t *prop_transition_point = + obs_properties_get(ppts, "transition_point"); if (type == TIMING_TIME) { obs_property_set_description( - p, obs_module_text("TransitionPoint")); - obs_property_int_set_suffix(p, " ms"); + prop_transition_point, + obs_module_text("TransitionPoint")); + } else { + obs_property_set_description( + prop_transition_point, + obs_module_text("TransitionPointFrame")); + } + + bool uses_ms_prefix = (type == TIMING_TIME); + obs_property_int_set_suffix(p, (uses_ms_prefix ? " ms" : "")); + + return true; +} + +static bool track_matte_layout_modified(obs_properties_t *ppts, + obs_property_t *p, obs_data_t *s) +{ + int matte_layout = obs_data_get_int(s, "track_matte_layout"); + obs_property_t *prop_matte_path = + obs_properties_get(ppts, "track_matte_path"); + + bool uses_separate_file = (matte_layout == MATTE_LAYOUT_SEPARATE_FILE); + obs_property_set_visible(prop_matte_path, uses_separate_file); + + UNUSED_PARAMETER(p); + return true; +} + +static bool track_matte_enabled_modified(obs_properties_t *ppts, + obs_property_t *p, obs_data_t *s) +{ + bool track_matte_enabled = obs_data_get_bool(s, "track_matte_enabled"); + obs_property_t *prop_tp_type = obs_properties_get(ppts, "tp_type"); + + if (track_matte_enabled) { + obs_property_set_description( + prop_tp_type, + obs_module_text("AudioTransitionPointType")); } else { obs_property_set_description( - p, obs_module_text("TransitionPointFrame")); - obs_property_int_set_suffix(p, ""); + prop_tp_type, obs_module_text("TransitionPointType")); } + + UNUSED_PARAMETER(p); return true; } @@ -324,8 +536,10 @@ static obs_properties_t *stinger_properties(void *data) obs_properties_set_flags(ppts, OBS_PROPERTIES_DEFER_UPDATE); + // main stinger settings obs_properties_add_path(ppts, "path", obs_module_text("VideoFile"), OBS_PATH_FILE, FILE_FILTER, NULL); + obs_property_t *p = obs_properties_add_list( ppts, "tp_type", obs_module_text("TransitionPointType"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); @@ -344,6 +558,45 @@ static obs_properties_t *stinger_properties(void *data) obs_module_text("TransitionPoint"), 0, 120000, 1); + // track matte properties + { + obs_properties_t *track_matte_group = obs_properties_create(); + + p = obs_properties_add_list(track_matte_group, + "track_matte_layout", + obs_module_text("TrackMatteLayout"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_list_add_int( + p, obs_module_text("TrackMatteLayoutHorizontal"), + MATTE_LAYOUT_HORIZONTAL); + obs_property_list_add_int( + p, obs_module_text("TrackMatteLayoutVertical"), + MATTE_LAYOUT_VERTICAL); + obs_property_list_add_int( + p, obs_module_text("TrackMatteLayoutSeparateFile"), + MATTE_LAYOUT_SEPARATE_FILE); + + obs_property_set_modified_callback(p, + track_matte_layout_modified); + + obs_properties_add_path(track_matte_group, "track_matte_path", + obs_module_text("TrackMatteVideoFile"), + OBS_PATH_FILE, FILE_FILTER, NULL); + + obs_properties_add_bool(track_matte_group, "invert_matte", + obs_module_text("InvertTrackMatte")); + + p = obs_properties_add_group( + ppts, "track_matte_enabled", + obs_module_text("TrackMatteEnabled"), + OBS_GROUP_CHECKABLE, track_matte_group); + + obs_property_set_modified_callback( + p, track_matte_enabled_modified); + } + + // audio output settings obs_property_t *monitor_list = obs_properties_add_list( ppts, "audio_monitoring", obs_module_text("AudioMonitoring"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); @@ -357,6 +610,7 @@ static obs_properties_t *stinger_properties(void *data) obs_module_text("AudioMonitoring.Both"), OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT); + // audio fade settings obs_property_t *audio_fade_style = obs_properties_add_list( ppts, "audio_fade_style", obs_module_text("AudioFadeStyle"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); -- GitLab