From 12764a1a2286bcb64761f6623b0a3ced15f0dbda Mon Sep 17 00:00:00 2001 From: "evan.liu" <583229071@qq.com> Date: Thu, 24 Sep 2026 08:04:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84=E5=A4=84?= =?UTF-8?q?=E7=90=86=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/HUD_Overlay_Color_v2.lua | 118 ++++---- deploy/HUD_Overlay_Outline.lua | 458 ++++++++++++++++++++++++++++++++ 2 files changed, 521 insertions(+), 55 deletions(-) create mode 100644 deploy/HUD_Overlay_Outline.lua diff --git a/deploy/HUD_Overlay_Color_v2.lua b/deploy/HUD_Overlay_Color_v2.lua index 2a34b2a..9077096 100644 --- a/deploy/HUD_Overlay_Color_v2.lua +++ b/deploy/HUD_Overlay_Color_v2.lua @@ -1,6 +1,9 @@ -- HUD Overlay Color Script for OBS (separate Create / Update buttons) --- Green-screen style keying: keeps only the green symbols and removes all other colors (including black) + custom symbol color --- Principle: greenness = G - max(R, B); green symbols are positive, black/gray/white backgrounds are ~0 +-- Black-to-transparent keying: the black background becomes fully transparent and +-- symbol pixels are drawn with a pure preset color (default pure green). +-- Principle: subtract a black level (kills the codec noise floor), use max(R, G, B) as +-- alpha (x gain) so anti-aliased edges survive as partial alpha instead of being eroded, +-- then output the preset color x alpha (premultiplied). -- Requires: obs-shaderfilter plugin (installed) -- Install: OBS -> Tools -> Scripts -> + -> select this file -> pick a color -- "Create Scene" : builds the scene from scratch (deletes and recreates both media sources) @@ -8,59 +11,60 @@ obs = obslua -local SETTING_FRONT_PATH = "front_path" -local SETTING_BACK_PATH = "back_path" -local SETTING_COLOR = "hud_color" -- preset color index -local SETTING_KEY_LOW = "key_low" -local SETTING_KEY_HIGH = "key_high" -local SETTING_OPACITY = "opacity" +local SETTING_FRONT_PATH = "front_path" +local SETTING_BACK_PATH = "back_path" +local SETTING_COLOR = "hud_color" -- preset color index (1 = keep original green) +local SETTING_BLACK_LEVEL = "black_level" +local SETTING_GAIN = "alpha_gain" +local SETTING_OPACITY = "opacity" local SCENE_NAME = "HUD Overlay Color" local FRONT_NAME = "HUD - Front Color" local BACK_NAME = "Center IG - Back Color" local FILTER_NAME = "HUD Colorize" --- Preset colors (ordered from strongest to weakest contrast against a teal background) +-- Preset colors; index 1 is pure green (the original HUD color) local COLOR_PRESETS = { - { name = "Red (recommended, complementary to teal)", r = 1.0, g = 0.0, b = 0.0 }, - { name = "Magenta", r = 1.0, g = 0.0, b = 1.0 }, - { name = "Orange", r = 1.0, g = 0.5, b = 0.0 }, - { name = "Yellow", r = 1.0, g = 1.0, b = 0.0 }, - { name = "White", r = 1.0, g = 1.0, b = 1.0 }, - { name = "Green (original color)", r = 0.0, g = 1.0, b = 0.0 }, - { name = "Bright green (neon green)", r = 0.25, g = 1.0, b = 0.1 }, - { name = "Black (strong contrast on light backgrounds)", r = 0.0, g = 0.0, b = 0.0 }, + { name = "Green (original HUD color)", r = 0.0, g = 1.0, b = 0.0 }, + { name = "Red (recommended, complementary to teal)", r = 1.0, g = 0.0, b = 0.0 }, + { name = "Magenta", r = 1.0, g = 0.0, b = 1.0 }, + { name = "Orange", r = 1.0, g = 0.5, b = 0.0 }, + { name = "Yellow", r = 1.0, g = 1.0, b = 0.0 }, + { name = "White", r = 1.0, g = 1.0, b = 1.0 }, + { name = "Bright green (neon green)", r = 0.25, g = 1.0, b = 0.1 }, + { name = "Black (strong contrast on light backgrounds)", r = 0.0, g = 0.0, b = 0.0 }, } local front_path = "" local back_path = "" local color_index = 1 -local key_low = 0.02 -local key_high = 0.20 -local opacity = 1.0 +local black_level = 0.055 -- 14/255, measured noise floor of the HUD capture +local alpha_gain = 1.35 +local opacity = 1.0 local fit_retries = 0 --- Green-preserving keying + recolor shader: --- greenness = G - max(R, B); only greenish pixels are positive --- alpha = smoothstep(key_low, key_high, greenness); soft edges preserve the anti-aliased transition --- Color is forced to the specified HUD color (premultiplied output) +-- Black-key shader: +-- h = max(px - black_level, 0) -> drop the noise floor, pure black becomes 0 +-- k = max(h.r, h.g, h.b) -> brightness of the symbol at this pixel +-- a = clamp(k * alpha_gain) -> soft alpha; anti-aliased edges keep partial alpha +-- Output is premultiplied: preset color x alpha (symbol pixels are pure HUD color) local SHADER_TEXT = [==[ -uniform float key_low < - string label = "Key low"; +uniform float black_level < + string label = "Black level"; string widget_type = "slider"; float minimum = 0.0; - float maximum = 0.5; + float maximum = 0.3; float step = 0.001; -> = 0.02; +> = 0.055; -uniform float key_high < - string label = "Key high"; +uniform float alpha_gain < + string label = "Alpha gain"; string widget_type = "slider"; - float minimum = 0.01; - float maximum = 1.0; - float step = 0.005; -> = 0.20; + float minimum = 0.5; + float maximum = 4.0; + float step = 0.01; +> = 1.35; uniform float hud_r < string label = "HUD color R"; @@ -68,7 +72,7 @@ uniform float hud_r < float minimum = 0.0; float maximum = 1.0; float step = 0.01; -> = 1.0; +> = 0.0; uniform float hud_g < string label = "HUD color G"; @@ -76,7 +80,7 @@ uniform float hud_g < float minimum = 0.0; float maximum = 1.0; float step = 0.01; -> = 0.0; +> = 1.0; uniform float hud_b < string label = "HUD color B"; @@ -101,13 +105,16 @@ float4 mainImage(VertData v_in) : TARGET // The OBS filter chain input is premultiplied; un-premultiply first (media sources have alpha=1, so usually no effect) px.rgb *= (px.a > 0.0) ? (1.0 / px.a) : 0.0; - // Greenness: only green symbols are positive; black/gray/white backgrounds are ~0 - float greenness = px.g - max(px.r, px.b); + // Subtract the black level: pure black (and the codec noise floor) becomes exactly 0 + float3 h = max(px.rgb - black_level, 0.0); - // Soft-threshold keying: fully transparent below key_low, fully opaque above key_high - float a = smoothstep(key_low, max(key_high, key_low + 0.001), greenness) * opacity; + // Brightness key: 0 for black, ~1.0 for pure green symbols + float k = max(h.r, max(h.g, h.b)); - // Premultiplied output: specified color x alpha + // Soft alpha with gain: thin strokes become more solid without eroding their edges + float a = clamp(k * alpha_gain, 0.0, 1.0) * opacity; + + // Premultiplied output: pure HUD color x alpha return float4(float3(hud_r, hud_g, hud_b) * a, a); } ]==] @@ -156,8 +163,8 @@ local function apply_key_filter(src) 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, "key_low", key_low) - obs.obs_data_set_double(fs, "key_high", key_high) + 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, "hud_r", preset.r) obs.obs_data_set_double(fs, "hud_g", preset.g) obs.obs_data_set_double(fs, "hud_b", preset.b) @@ -339,18 +346,19 @@ end ------------------------------------------------------------ function script_description() - return "Green-screen style keying: keeps only the green symbols and removes all other colors (including black).\n" .. - "Principle: greenness = G - max(R, B), which is less likely to pick up background noise than the old 'use the G channel as alpha' approach.\n" .. + return "Black-to-transparent keying: the black background becomes fully transparent and symbol pixels are drawn with a pure preset color (default pure green).\n" .. + "alpha = max(R, G, B) after subtracting a black level, so anti-aliased edges survive as partial alpha instead of being eroded.\n" .. + "Compared with the old greenness keying, thin lines and small text no longer fade or break apart.\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, symbol color, keying parameters).\n" .. - "The filter panel also has HUD color R/G/B sliders for fine-tuning to any color." + "The filter panel also has Black level / Alpha gain / HUD color R/G/B sliders for fine-tuning." end function script_properties() local props = obs.obs_properties_create() obs.obs_properties_add_path(props, SETTING_FRONT_PATH, - "Foreground video (HUD, green symbols)", obs.OBS_PATH_FILE, + "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, @@ -363,10 +371,10 @@ function script_properties() obs.obs_property_list_add_int(clist, c.name, i) end - obs.obs_properties_add_float_slider(props, SETTING_KEY_LOW, - "Green lower bound (increase if noise/color artifacts remain)", 0.0, 0.5, 0.001) - obs.obs_properties_add_float_slider(props, SETTING_KEY_HIGH, - "Green upper bound (decrease if thin lines fade or edges get too soft)", 0.01, 1.0, 0.005) + obs.obs_properties_add_float_slider(props, SETTING_BLACK_LEVEL, + "Black level (increase if a dark halo/noise remains around the symbols)", 0.0, 0.3, 0.001) + obs.obs_properties_add_float_slider(props, SETTING_GAIN, + "Alpha gain (increase if the symbols look too transparent over bright sky)", 0.5, 4.0, 0.01) obs.obs_properties_add_float_slider(props, SETTING_OPACITY, "Overall opacity", 0.0, 1.0, 0.01) @@ -382,8 +390,8 @@ function script_defaults(settings) obs.obs_data_set_default_string(settings, SETTING_BACK_PATH, "D:\\Test Pictures + Video\\Center IG - Back Videos.avi") obs.obs_data_set_default_int(settings, SETTING_COLOR, 1) - obs.obs_data_set_default_double(settings, SETTING_KEY_LOW, 0.02) - obs.obs_data_set_default_double(settings, SETTING_KEY_HIGH, 0.20) + 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_OPACITY, 1.0) end @@ -391,7 +399,7 @@ 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) color_index = obs.obs_data_get_int(settings, SETTING_COLOR) - key_low = obs.obs_data_get_double(settings, SETTING_KEY_LOW) - key_high = obs.obs_data_get_double(settings, SETTING_KEY_HIGH) + black_level = obs.obs_data_get_double(settings, SETTING_BLACK_LEVEL) + alpha_gain = obs.obs_data_get_double(settings, SETTING_GAIN) opacity = obs.obs_data_get_double(settings, SETTING_OPACITY) end diff --git a/deploy/HUD_Overlay_Outline.lua b/deploy/HUD_Overlay_Outline.lua new file mode 100644 index 0000000..f3d2d18 --- /dev/null +++ b/deploy/HUD_Overlay_Outline.lua @@ -0,0 +1,458 @@ +-- 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