-- HUD Overlay Outline Script for OBS (separate Create / Update buttons) -- New approach: detect the green symbols first, then draw them in pure green with a -- dark green outline, so they stay readable over ANY background (bright sky, green terrain, clouds). -- Principle: -- 1. symbol detection: subtract a black level, alpha = max(R, G, B) x gain, -- multiplied by a greenness factor (G - max(R, B)) to reject gray/white noise -- 2. outline: sample the symbol alpha in a ring around each pixel; pixels that are -- near a symbol but not part of it get a dark outline underneath the green fill -- 3. premultiplied output: pure green x symbol alpha + outline color x outline alpha -- Requires: obs-shaderfilter plugin (installed) -- Install: OBS -> Tools -> Scripts -> + -> select this file -- "Create Scene" : builds the scene from scratch (deletes and recreates both media sources) -- "Update Scene" : updates the existing scene in place (video paths, color, keying parameters) obs = obslua local SETTING_FRONT_PATH = "front_path" local SETTING_BACK_PATH = "back_path" local SETTING_BLACK_LEVEL = "black_level" local SETTING_GAIN = "alpha_gain" local SETTING_GREEN_THRESH = "green_thresh" local SETTING_OUT_WIDTH = "outline_width" local SETTING_OUT_ALPHA = "outline_alpha" local SETTING_OPACITY = "opacity" local SCENE_NAME = "HUD Overlay Outline" local FRONT_NAME = "HUD - Front Outline" local BACK_NAME = "Center IG - Back Outline" local FILTER_NAME = "HUD Outline" local front_path = "" local back_path = "" local black_level = 0.055 -- 14/255, measured noise floor of the HUD capture local alpha_gain = 1.35 local green_thresh = 0.10 -- greenness (G - max(R,B)) needed to count as a green symbol local outline_width = 2.0 -- outline thickness in source pixels local outline_alpha = 0.9 local opacity = 1.0 local fit_retries = 0 -- Symbol-detect + outline shader: -- sym_alpha(uv): black level subtraction -> brightness key x gain -> greenness gate -- main: symbol = pure green x sym_alpha; ring of neighbor sym_alpha forms the outline local SHADER_TEXT = [==[ uniform float black_level < string label = "Black level"; string widget_type = "slider"; float minimum = 0.0; float maximum = 0.3; float step = 0.001; > = 0.055; uniform float alpha_gain < string label = "Alpha gain"; string widget_type = "slider"; float minimum = 0.5; float maximum = 4.0; float step = 0.01; > = 1.35; uniform float green_thresh < string label = "Green threshold"; string widget_type = "slider"; float minimum = 0.01; float maximum = 0.5; float step = 0.005; > = 0.10; uniform float outline_width < string label = "Outline width (px)"; string widget_type = "slider"; float minimum = 0.0; float maximum = 8.0; float step = 0.5; > = 2.0; uniform float outline_alpha < string label = "Outline strength"; string widget_type = "slider"; float minimum = 0.0; float maximum = 1.0; float step = 0.01; > = 0.9; uniform float hud_r < string label = "Symbol color R"; string widget_type = "slider"; float minimum = 0.0; float maximum = 1.0; float step = 0.01; > = 0.0; uniform float hud_g < string label = "Symbol color G"; string widget_type = "slider"; float minimum = 0.0; float maximum = 1.0; float step = 0.01; > = 1.0; uniform float hud_b < string label = "Symbol color B"; string widget_type = "slider"; float minimum = 0.0; float maximum = 1.0; float step = 0.01; > = 0.0; uniform float outline_brightness < string label = "Outline brightness"; string widget_type = "slider"; float minimum = 0.0; float maximum = 1.0; float step = 0.01; > = 0.35; uniform float opacity < string label = "Opacity"; string widget_type = "slider"; float minimum = 0.0; float maximum = 1.0; float step = 0.01; > = 1.0; // Symbol alpha at a given UV: 1 = solid green symbol, 0 = background/noise float sym_alpha(float2 uv) { float4 px = image.Sample(textureSampler, uv); px.rgb *= (px.a > 0.0) ? (1.0 / px.a) : 0.0; // drop the codec noise floor; pure black becomes exactly 0 float3 h = max(px.rgb - black_level, 0.0); // brightness key float k = max(h.r, max(h.g, h.b)); // greenness gate: green symbols pass, gray/white noise is rejected float gn = h.g - max(h.r, h.b); float gf = clamp(gn / green_thresh, 0.0, 1.0); return clamp(k * alpha_gain, 0.0, 1.0) * gf; } float4 mainImage(VertData v_in) : TARGET { float a_sym = sym_alpha(v_in.uv); // Ring of 8 neighbor taps at outline_width distance -> dilated symbol alpha float2 d = uv_pixel_interval * outline_width; float2 dg = d * 0.7071; float ring = 0.0; ring = max(ring, sym_alpha(v_in.uv + float2( d.x, 0.0))); ring = max(ring, sym_alpha(v_in.uv + float2(-d.x, 0.0))); ring = max(ring, sym_alpha(v_in.uv + float2( 0.0, d.y))); ring = max(ring, sym_alpha(v_in.uv + float2( 0.0, -d.y))); ring = max(ring, sym_alpha(v_in.uv + float2( dg.x, dg.y))); ring = max(ring, sym_alpha(v_in.uv + float2(-dg.x, dg.y))); ring = max(ring, sym_alpha(v_in.uv + float2( dg.x, -dg.y))); ring = max(ring, sym_alpha(v_in.uv + float2(-dg.x, -dg.y))); // Outline only where the symbol itself is not already opaque float a_out = min(ring * outline_alpha, 1.0 - a_sym); float a_sym_o = a_sym * opacity; float a_out_o = a_out * opacity; // Premultiplied: pure symbol color over a dimmer pure-green outline float3 rgb = float3(hud_r, hud_g, hud_b) * a_sym_o + float3(hud_r, hud_g, hud_b) * outline_brightness * a_out_o; return float4(rgb, a_sym_o + a_out_o); } ]==] ------------------------------------------------------------ -- Utility functions ------------------------------------------------------------ local function remove_source_by_name(name) local s = obs.obs_get_source_by_name(name) if s ~= nil then obs.obs_source_remove(s) obs.obs_source_release(s) end end local function create_media_source(name, path) local s = obs.obs_data_create() obs.obs_data_set_string(s, "local_file", path) obs.obs_data_set_bool(s, "is_local_file", true) obs.obs_data_set_bool(s, "looping", true) obs.obs_data_set_bool(s, "restart_on_activate", true) obs.obs_data_set_bool(s, "close_when_inactive", false) local src = obs.obs_source_create("ffmpeg_source", name, s, nil) obs.obs_data_release(s) return src end -- Point an existing media source at a new file without recreating it local function update_media_source_path(src, path) local s = obs.obs_source_get_settings(src) obs.obs_data_set_string(s, "local_file", path) obs.obs_source_update(src, s) obs.obs_data_release(s) end local function apply_key_filter(src) local old = obs.obs_source_get_filter_by_name(src, FILTER_NAME) if old ~= nil then obs.obs_source_filter_remove(src, old) obs.obs_source_release(old) end local fs = obs.obs_data_create() obs.obs_data_set_bool(fs, "from_file", false) obs.obs_data_set_string(fs, "shader_text", SHADER_TEXT) obs.obs_data_set_double(fs, "black_level", black_level) obs.obs_data_set_double(fs, "alpha_gain", alpha_gain) obs.obs_data_set_double(fs, "green_thresh", green_thresh) obs.obs_data_set_double(fs, "outline_width", outline_width) obs.obs_data_set_double(fs, "outline_alpha", outline_alpha) obs.obs_data_set_double(fs, "opacity", opacity) -- First check whether the obs-shaderfilter plugin is loaded if obs.obs_source_get_display_name("shader_filter") == nil then obs.obs_data_release(fs) obs.script_log(obs.LOG_WARNING, "HUD Overlay Outline: obs-shaderfilter plugin not detected; keying was not applied. Please fully quit OBS (including the system tray) and restart it.") return end local f = obs.obs_source_create("shader_filter", FILTER_NAME, fs, nil) obs.obs_data_release(fs) if f ~= nil then obs.obs_source_filter_add(src, f) obs.obs_source_release(f) end end local function fit_items_to_canvas() local vi = obs.obs_video_info() if obs.obs_get_video_info(vi) == false then return false end local cw, ch = vi.base_width, vi.base_height local scene_src = obs.obs_get_source_by_name(SCENE_NAME) if scene_src == nil then return false end local scene = obs.obs_scene_from_source(scene_src) local items = obs.obs_scene_enum_items(scene) local all_ready = true for _, item in ipairs(items) do local s = obs.obs_sceneitem_get_source(item) local w = obs.obs_source_get_width(s) local h = obs.obs_source_get_height(s) if w > 0 and h > 0 then local scale = math.min(cw / w, ch / h) local sc = obs.vec2() sc.x = scale sc.y = scale obs.obs_sceneitem_set_scale(item, sc) local pos = obs.vec2() pos.x = (cw - w * scale) / 2 pos.y = (ch - h * scale) / 2 obs.obs_sceneitem_set_pos(item, pos) -- Use Lanczos when upscaling line-art footage; sharper than the default bilinear if obs.obs_sceneitem_set_scale_filter ~= nil then obs.obs_sceneitem_set_scale_filter(item, obs.OBS_SCALE_LANCZOS or 4) end else all_ready = false end end obs.sceneitem_list_release(items) obs.obs_source_release(scene_src) return all_ready end local function fit_retry() fit_retries = fit_retries - 1 if fit_items_to_canvas() or fit_retries <= 0 then obs.timer_remove(fit_retry) end end local function schedule_fit() fit_retries = 10 obs.timer_remove(fit_retry) obs.timer_add(fit_retry, 500) end ------------------------------------------------------------ -- Create scene (from scratch) ------------------------------------------------------------ local function create_scene() if front_path == "" or back_path == "" then obs.script_log(obs.LOG_WARNING, "HUD Overlay Outline: please set the foreground and background video paths first") return end local scene = nil local scene_src = obs.obs_get_source_by_name(SCENE_NAME) if scene_src ~= nil then scene = obs.obs_scene_from_source(scene_src) else scene = obs.obs_scene_create(SCENE_NAME) scene_src = obs.obs_scene_get_source(scene) end remove_source_by_name(FRONT_NAME) remove_source_by_name(BACK_NAME) local back = create_media_source(BACK_NAME, back_path) obs.obs_scene_add(scene, back) local front = create_media_source(FRONT_NAME, front_path) apply_key_filter(front) obs.obs_scene_add(scene, front) obs.obs_source_release(back) obs.obs_source_release(front) obs.obs_frontend_set_current_scene(scene_src) obs.obs_source_release(scene_src) schedule_fit() obs.script_log(obs.LOG_INFO, "HUD Overlay Outline: scene created") end ------------------------------------------------------------ -- Update scene (in place, without recreating sources) ------------------------------------------------------------ local function update_scene() if front_path == "" or back_path == "" then obs.script_log(obs.LOG_WARNING, "HUD Overlay Outline: please set the foreground and background video paths first") return end local scene_src = obs.obs_get_source_by_name(SCENE_NAME) if scene_src == nil then obs.script_log(obs.LOG_WARNING, "HUD Overlay Outline: scene \"" .. SCENE_NAME .. "\" does not exist yet; please use \"Create Scene\" first") return end local back = obs.obs_get_source_by_name(BACK_NAME) local front = obs.obs_get_source_by_name(FRONT_NAME) if back == nil or front == nil then if back ~= nil then obs.obs_source_release(back) end if front ~= nil then obs.obs_source_release(front) end obs.obs_source_release(scene_src) obs.script_log(obs.LOG_WARNING, "HUD Overlay Outline: media sources are missing; please use \"Create Scene\" to rebuild the scene") return end -- Update video files in place update_media_source_path(back, back_path) update_media_source_path(front, front_path) -- Re-apply the keying filter with the current keying parameters apply_key_filter(front) obs.obs_source_release(back) obs.obs_source_release(front) obs.obs_frontend_set_current_scene(scene_src) obs.obs_source_release(scene_src) schedule_fit() obs.script_log(obs.LOG_INFO, "HUD Overlay Outline: scene updated") end local function on_create_clicked(props, p) create_scene() return true end local function on_update_clicked(props, p) update_scene() return true end ------------------------------------------------------------ -- Script UI and settings ------------------------------------------------------------ function script_description() return "Detects the green HUD symbols, then draws them in pure green with a dark green outline,\n" .. "so they stay readable over any background (bright sky, green terrain, clouds).\n" .. "Symbol detection: alpha = max(R, G, B) x gain after black-level subtraction, gated by greenness G - max(R, B) to reject noise.\n" .. "\"Create Scene\": builds the scene from scratch (deletes and recreates both media sources).\n" .. "\"Update Scene\": updates the existing scene in place (video paths, keying parameters).\n" .. "The filter panel has sliders for symbol color, outline width, strength and brightness." end function script_properties() local props = obs.obs_properties_create() obs.obs_properties_add_path(props, SETTING_FRONT_PATH, "Foreground video (HUD, green symbols on black)", obs.OBS_PATH_FILE, "Video files (*.avi *.mp4 *.mov *.mkv *.webm);;All files (*.*)", "") obs.obs_properties_add_path(props, SETTING_BACK_PATH, "Background video (bottom layer)", obs.OBS_PATH_FILE, "Video files (*.avi *.mp4 *.mov *.mkv *.webm);;All files (*.*)", "") obs.obs_properties_add_float_slider(props, SETTING_BLACK_LEVEL, "Black level (increase if a dark halo/noise remains)", 0.0, 0.3, 0.001) obs.obs_properties_add_float_slider(props, SETTING_GAIN, "Alpha gain (increase if symbols look too transparent)", 0.5, 4.0, 0.01) obs.obs_properties_add_float_slider(props, SETTING_GREEN_THRESH, "Green threshold (increase if non-green noise appears)", 0.01, 0.5, 0.005) obs.obs_properties_add_float_slider(props, SETTING_OUT_WIDTH, "Outline width (px)", 0.0, 8.0, 0.5) obs.obs_properties_add_float_slider(props, SETTING_OUT_ALPHA, "Outline strength", 0.0, 1.0, 0.01) obs.obs_properties_add_float_slider(props, SETTING_OPACITY, "Overall opacity", 0.0, 1.0, 0.01) obs.obs_properties_add_button(props, "create_btn", "Create Scene", on_create_clicked) obs.obs_properties_add_button(props, "update_btn", "Update Scene", on_update_clicked) return props end function script_defaults(settings) obs.obs_data_set_default_string(settings, SETTING_FRONT_PATH, "D:\\Test Pictures + Video\\HUD - Front Video.avi") obs.obs_data_set_default_string(settings, SETTING_BACK_PATH, "D:\\Test Pictures + Video\\Center IG - Back Videos.avi") obs.obs_data_set_default_double(settings, SETTING_BLACK_LEVEL, 0.055) obs.obs_data_set_default_double(settings, SETTING_GAIN, 1.35) obs.obs_data_set_default_double(settings, SETTING_GREEN_THRESH, 0.10) obs.obs_data_set_default_double(settings, SETTING_OUT_WIDTH, 2.0) obs.obs_data_set_default_double(settings, SETTING_OUT_ALPHA, 0.9) obs.obs_data_set_default_double(settings, SETTING_OPACITY, 1.0) end function script_update(settings) front_path = obs.obs_data_get_string(settings, SETTING_FRONT_PATH) back_path = obs.obs_data_get_string(settings, SETTING_BACK_PATH) black_level = obs.obs_data_get_double(settings, SETTING_BLACK_LEVEL) alpha_gain = obs.obs_data_get_double(settings, SETTING_GAIN) green_thresh = obs.obs_data_get_double(settings, SETTING_GREEN_THRESH) outline_width = obs.obs_data_get_double(settings, SETTING_OUT_WIDTH) outline_alpha = obs.obs_data_get_double(settings, SETTING_OUT_ALPHA) opacity = obs.obs_data_get_double(settings, SETTING_OPACITY) end