From 054bef361c1b7c8f910b7b15e17b1a13612a6015 Mon Sep 17 00:00:00 2001
From: "evan.liu" <583229071@qq.com>
Date: Sat, 3 Oct 2026 10:12:38 +0800
Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E9=80=81Ai=E6=BC=8F=E6=8E=89=E7=9A=84?=
=?UTF-8?q?=E6=96=87=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
deploy/HUD_Overlay_BlackKey.lua | 406 ++++++++++++
...tline.lua => HUD_Overlay_Outline-V1.0.lua} | 111 +++-
deploy/HUD_Overlay_Outline-V2.lua | 605 ++++++++++++++++++
3 files changed, 1093 insertions(+), 29 deletions(-)
create mode 100644 deploy/HUD_Overlay_BlackKey.lua
rename deploy/{HUD_Overlay_Outline.lua => HUD_Overlay_Outline-V1.0.lua} (74%)
create mode 100644 deploy/HUD_Overlay_Outline-V2.lua
diff --git a/deploy/HUD_Overlay_BlackKey.lua b/deploy/HUD_Overlay_BlackKey.lua
new file mode 100644
index 0000000..bc00694
--- /dev/null
+++ b/deploy/HUD_Overlay_BlackKey.lua
@@ -0,0 +1,406 @@
+-- HUD Overlay Black-Key Script for OBS (separate Create / Update buttons)
+-- Black-to-transparent keying: black pixels become fully transparent, brightness is used as alpha
+-- Principle: alpha = max(R, G, B); pure black -> alpha 0 (transparent), the brighter the pixel the more opaque
+-- For a "pure green on pure black" HUD this is simpler and more stable than greenness keying:
+-- no green judgment at all, so anti-aliased edges and dim segments key out naturally.
+-- Requires: obs-shaderfilter plugin (installed)
+-- Install: OBS -> Tools -> Scripts -> + -> select this file -> pick a mode
+-- "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_COLOR = "hud_color" -- preset color index (1 = keep original green)
+local SETTING_BLACK_LOW = "black_low"
+local SETTING_GAIN = "alpha_gain"
+local SETTING_OPACITY = "opacity"
+
+local SCENE_NAME = "HUD Overlay Black Key"
+local FRONT_NAME = "HUD - Front BlackKey"
+local BACK_NAME = "Center IG - Back BlackKey"
+local FILTER_NAME = "Black Key"
+
+-- Preset colors; index 1 keeps the original green from the video
+local COLOR_PRESETS = {
+ { name = "Keep original color (video green)", r = 0.0, g = 1.0, b = 0.0, original = true },
+ { name = "Red (recommended, complementary to teal)", r = 1.0, g = 0.0, b = 0.0, original = false },
+ { name = "Magenta", r = 1.0, g = 0.0, b = 1.0, original = false },
+ { name = "Orange", r = 1.0, g = 0.5, b = 0.0, original = false },
+ { name = "Yellow", r = 1.0, g = 1.0, b = 0.0, original = false },
+ { name = "White", r = 1.0, g = 1.0, b = 1.0, original = false },
+ { name = "Bright green (neon green)", r = 0.25, g = 1.0, b = 0.1, original = false },
+}
+
+local front_path = ""
+local back_path = ""
+local color_index = 1
+local black_low = 0.0
+local alpha_gain = 1.0
+local opacity = 1.0
+
+local fit_retries = 0
+
+-- Black-to-transparent keying shader:
+-- k = max(R, G, B); black -> 0, pure green -> 1.0
+-- alpha = rescale k above black_low, then apply gain; soft alpha preserves anti-aliased edges
+-- Output is premultiplied: either the original pixel color or the specified HUD color
+local SHADER_TEXT = [==[
+uniform float black_low <
+ string label = "Black threshold";
+ string widget_type = "slider";
+ float minimum = 0.0;
+ float maximum = 0.5;
+ float step = 0.001;
+> = 0.0;
+
+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.0;
+
+uniform bool use_original <
+ string label = "Keep original color";
+> = true;
+
+uniform float hud_r <
+ string label = "HUD 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 = "HUD 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 = "HUD color B";
+ string widget_type = "slider";
+ float minimum = 0.0;
+ float maximum = 1.0;
+ float step = 0.01;
+> = 0.0;
+
+uniform float opacity <
+ string label = "Opacity";
+ string widget_type = "slider";
+ float minimum = 0.0;
+ float maximum = 1.0;
+ float step = 0.01;
+> = 1.0;
+
+float4 mainImage(VertData v_in) : TARGET
+{
+ float4 px = image.Sample(textureSampler, v_in.uv);
+
+ // 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;
+
+ // Brightness key: max channel is 0 for black, 1.0 for pure green
+ float k = max(px.r, max(px.g, px.b));
+
+ // Everything below black_low is fully transparent; rescale the rest back to [0, 1]
+ float a = clamp((k - black_low) / max(1.0 - black_low, 0.001), 0.0, 1.0);
+ a = clamp(a * alpha_gain, 0.0, 1.0) * opacity;
+
+ float3 col = use_original ? px.rgb : float3(hud_r, hud_g, hud_b);
+
+ // Premultiplied output: color x alpha
+ return float4(col * a, a);
+}
+]==]
+
+------------------------------------------------------------
+-- 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 preset = COLOR_PRESETS[color_index] or COLOR_PRESETS[1]
+
+ 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_low", black_low)
+ obs.obs_data_set_double(fs, "alpha_gain", alpha_gain)
+ obs.obs_data_set_bool(fs, "use_original", preset.original)
+ 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)
+ 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 Black Key: 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 Black Key: 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 Black Key: scene created, symbol color = " ..
+ (COLOR_PRESETS[color_index] or COLOR_PRESETS[1]).name)
+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 Black Key: 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 Black Key: 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 Black Key: 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 color / 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 Black Key: scene updated, symbol color = " ..
+ (COLOR_PRESETS[color_index] or COLOR_PRESETS[1]).name)
+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 "Black-to-transparent keying: black pixels become fully transparent; pixel brightness (max of R/G/B) is used as alpha.\n" ..
+ "For a 'pure green on pure black' HUD this is simpler and more stable than greenness keying; edges and dim segments key out naturally.\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 Black threshold / Alpha gain / HUD color 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 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 (*.*)", "")
+
+ local clist = obs.obs_properties_add_list(props, SETTING_COLOR,
+ "Symbol color", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_INT)
+ for i, c in ipairs(COLOR_PRESETS) do
+ obs.obs_property_list_add_int(clist, c.name, i)
+ end
+
+ obs.obs_properties_add_float_slider(props, SETTING_BLACK_LOW,
+ "Black threshold (increase if dark noise remains around the symbols)", 0.0, 0.5, 0.001)
+ obs.obs_properties_add_float_slider(props, SETTING_GAIN,
+ "Alpha gain (increase if the symbols look too transparent)", 0.5, 4.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_int(settings, SETTING_COLOR, 1)
+ obs.obs_data_set_default_double(settings, SETTING_BLACK_LOW, 0.0)
+ obs.obs_data_set_default_double(settings, SETTING_GAIN, 1.0)
+ 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)
+ color_index = obs.obs_data_get_int(settings, SETTING_COLOR)
+ black_low = obs.obs_data_get_double(settings, SETTING_BLACK_LOW)
+ 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-V1.0.lua
similarity index 74%
rename from deploy/HUD_Overlay_Outline.lua
rename to deploy/HUD_Overlay_Outline-V1.0.lua
index f3d2d18..487c422 100644
--- a/deploy/HUD_Overlay_Outline.lua
+++ b/deploy/HUD_Overlay_Outline-V1.0.lua
@@ -1,12 +1,13 @@
-- 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).
+-- New approach: detect the green symbols first, then draw them in a configurable
+-- color (preset list x brightness), 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
+-- 2. optional outline: sample the symbol alpha in a ring around each pixel; pixels
+-- near a symbol get the same color (x outline brightness) underneath the fill
+-- 3. premultiplied output: symbol color 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)
@@ -16,6 +17,8 @@ obs = obslua
local SETTING_FRONT_PATH = "front_path"
local SETTING_BACK_PATH = "back_path"
+local SETTING_COLOR = "hud_color" -- preset color index
+local SETTING_BRIGHTNESS = "symbol_brightness"
local SETTING_BLACK_LEVEL = "black_level"
local SETTING_GAIN = "alpha_gain"
local SETTING_GREEN_THRESH = "green_thresh"
@@ -28,12 +31,27 @@ local FRONT_NAME = "HUD - Front Outline"
local BACK_NAME = "Center IG - Back Outline"
local FILTER_NAME = "HUD Outline"
+-- Preset symbol colors at full brightness; the Brightness slider scales the chosen color
+local COLOR_PRESETS = {
+ { name = "Pure green", r = 0.0, g = 1.0, b = 0.0 },
+ { name = "HUD green (measured from source video)", r = 0.15, g = 1.0, b = 0.10 },
+ { name = "Bright green (neon green)", r = 0.25, g = 1.0, b = 0.1 },
+ { name = "Red (complementary to teal sky)", 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 = "Black", r = 0.0, g = 0.0, b = 0.0 },
+}
+
local front_path = ""
local back_path = ""
+local color_index = 1
+local symbol_brightness = 1.0
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_width = 0.0 -- stroke width adjustment in source pixels: + thicker, - thinner, 0 = original
local outline_alpha = 0.9
local opacity = 1.0
@@ -68,12 +86,12 @@ uniform float green_thresh <
> = 0.10;
uniform float outline_width <
- string label = "Outline width (px)";
+ string label = "Symbol width (px, + thicker / - thinner)";
string widget_type = "slider";
- float minimum = 0.0;
+ float minimum = -4.0;
float maximum = 8.0;
float step = 0.5;
-> = 2.0;
+> = 0.0;
uniform float outline_alpha <
string label = "Outline strength";
@@ -113,7 +131,7 @@ uniform float outline_brightness <
float minimum = 0.0;
float maximum = 1.0;
float step = 0.01;
-> = 0.35;
+> = 1.0;
uniform float opacity <
string label = "Opacity";
@@ -146,26 +164,42 @@ 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;
+ // Symbol width: positive = dilate (thicker strokes), negative = erode (thinner)
+ float2 d = uv_pixel_interval * abs(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)));
+ float a_out = 0.0;
- // Outline only where the symbol itself is not already opaque
- float a_out = min(ring * outline_alpha, 1.0 - a_sym);
+ if (outline_width > 0.0) {
+ // Ring max -> same-color outline under the fill = thicker strokes
+ 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
+ a_out = min(ring * outline_alpha, 1.0 - a_sym);
+ } else if (outline_width < 0.0) {
+ // Ring min -> erode the strokes thinner
+ float core = 1.0;
+ core = min(core, sym_alpha(v_in.uv + float2( d.x, 0.0)));
+ core = min(core, sym_alpha(v_in.uv + float2(-d.x, 0.0)));
+ core = min(core, sym_alpha(v_in.uv + float2( 0.0, d.y)));
+ core = min(core, sym_alpha(v_in.uv + float2( 0.0, -d.y)));
+ core = min(core, sym_alpha(v_in.uv + float2( dg.x, dg.y)));
+ core = min(core, sym_alpha(v_in.uv + float2(-dg.x, dg.y)));
+ core = min(core, sym_alpha(v_in.uv + float2( dg.x, -dg.y)));
+ core = min(core, sym_alpha(v_in.uv + float2(-dg.x, -dg.y)));
+ a_sym = min(a_sym, core);
+ }
float a_sym_o = a_sym * opacity;
float a_out_o = a_out * opacity;
- // Premultiplied: pure symbol color over a dimmer pure-green outline
+ // Premultiplied: symbol color over a same-color outline (x outline brightness)
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);
@@ -211,9 +245,14 @@ local function apply_key_filter(src)
obs.obs_source_release(old)
end
+ local preset = COLOR_PRESETS[color_index] or COLOR_PRESETS[1]
+
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, "hud_r", preset.r * symbol_brightness)
+ obs.obs_data_set_double(fs, "hud_g", preset.g * symbol_brightness)
+ obs.obs_data_set_double(fs, "hud_b", preset.b * symbol_brightness)
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)
@@ -395,12 +434,14 @@ end
------------------------------------------------------------
function script_description()
- return "Detects the green HUD symbols, then draws them in pure green with a dark green outline,\n" ..
+ return "Detects the green HUD symbols and draws them in a configurable color and brightness,\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" ..
+ "\"Symbol color\" picks the hue, \"Symbol brightness\" scales it (0.35 = dark green, 1.0 = full brightness).\n" ..
+ "\"Symbol width\" thickens (positive) or thins (negative) the strokes; 0 keeps the original width.\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."
+ "\"Update Scene\": updates the existing scene in place (video paths, color, keying parameters).\n" ..
+ "The filter panel has sliders for fine-tuning (symbol color R/G/B, outline width, strength and brightness)."
end
function script_properties()
@@ -414,6 +455,14 @@ function script_properties()
"Background video (bottom layer)", obs.OBS_PATH_FILE,
"Video files (*.avi *.mp4 *.mov *.mkv *.webm);;All files (*.*)", "")
+ local clist = obs.obs_properties_add_list(props, SETTING_COLOR,
+ "Symbol color", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_INT)
+ for i, c in ipairs(COLOR_PRESETS) do
+ obs.obs_property_list_add_int(clist, c.name, i)
+ end
+
+ obs.obs_properties_add_float_slider(props, SETTING_BRIGHTNESS,
+ "Symbol brightness (scales the chosen color)", 0.05, 1.0, 0.01)
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,
@@ -421,7 +470,7 @@ function script_properties()
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)
+ "Symbol width (px, + thicker / - thinner, 0 = original)", -4.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,
@@ -438,10 +487,12 @@ function script_defaults(settings)
"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_int(settings, SETTING_COLOR, 1)
+ obs.obs_data_set_default_double(settings, SETTING_BRIGHTNESS, 1.0)
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_WIDTH, 0.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
@@ -449,6 +500,8 @@ 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)
+ color_index = obs.obs_data_get_int(settings, SETTING_COLOR)
+ symbol_brightness = obs.obs_data_get_double(settings, SETTING_BRIGHTNESS)
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)
diff --git a/deploy/HUD_Overlay_Outline-V2.lua b/deploy/HUD_Overlay_Outline-V2.lua
new file mode 100644
index 0000000..ac4740d
--- /dev/null
+++ b/deploy/HUD_Overlay_Outline-V2.lua
@@ -0,0 +1,605 @@
+-- HUD Overlay Outline Script for OBS (separate Create / Update buttons)
+-- New approach: detect the green symbols first, then draw them in a configurable
+-- color (preset list x brightness), 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. optional outline: sample the symbol alpha in a ring around each pixel; pixels
+-- near a symbol get the outline color (default blue, x outline brightness)
+-- underneath the fill
+-- 3. premultiplied output: symbol color 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_COLOR = "hud_color" -- preset color index
+local SETTING_BRIGHTNESS = "symbol_brightness"
+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_OUT_COLOR = "outline_color" -- outline preset color index (1 = blue)
+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"
+
+-- Preset symbol colors at full brightness; the Brightness slider scales the chosen color
+local COLOR_PRESETS = {
+ { name = "Pure green", r = 0.0, g = 1.0, b = 0.0 },
+ { name = "HUD green (measured from source video)", r = 0.15, g = 1.0, b = 0.10 },
+ { name = "Bright green (neon green)", r = 0.25, g = 1.0, b = 0.1 },
+ { name = "Red (complementary to teal sky)", 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 = "Black", r = 0.0, g = 0.0, b = 0.0 },
+}
+
+-- Preset outline colors; index 1 is blue (the outline default)
+local OUTLINE_COLOR_PRESETS = {
+ { name = "Blue", r = 0.0, g = 0.0, b = 1.0 },
+ { name = "Green", r = 0.0, g = 1.0, b = 0.0 },
+ { name = "Cyan", r = 0.0, g = 1.0, b = 1.0 },
+ { name = "White", r = 1.0, g = 1.0, b = 1.0 },
+ { name = "Black", r = 0.0, g = 0.0, b = 0.0 },
+ { name = "Red", r = 1.0, g = 0.0, b = 0.0 },
+ { name = "Yellow", r = 1.0, g = 1.0, b = 0.0 },
+}
+
+local front_path = ""
+local back_path = ""
+local color_index = 1
+local outline_color_index = 1
+local symbol_brightness = 1.0
+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 = 0.0 -- stroke width adjustment in source pixels: + thicker, - thinner, 0 = original
+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 = "Symbol width (px, + thicker / - thinner)";
+ string widget_type = "slider";
+ float minimum = -4.0;
+ float maximum = 8.0;
+ float step = 0.5;
+> = 0.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;
+> = 1.0;
+
+uniform float outline_r <
+ string label = "Outline color R";
+ string widget_type = "slider";
+ float minimum = 0.0;
+ float maximum = 1.0;
+ float step = 0.01;
+> = 0.0;
+
+uniform float outline_g <
+ string label = "Outline color G";
+ string widget_type = "slider";
+ float minimum = 0.0;
+ float maximum = 1.0;
+ float step = 0.01;
+> = 0.0;
+
+uniform float outline_b <
+ string label = "Outline color B";
+ string widget_type = "slider";
+ float minimum = 0.0;
+ float maximum = 1.0;
+ float step = 0.01;
+> = 1.0;
+
+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);
+
+ // Symbol width: positive = dilate (thicker strokes), negative = erode (thinner)
+ float2 d = uv_pixel_interval * abs(outline_width);
+ float2 dg = d * 0.7071;
+ float a_out = 0.0;
+
+ if (outline_width > 0.0) {
+ // Ring max -> same-color outline under the fill = thicker strokes
+ 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
+ a_out = min(ring * outline_alpha, 1.0 - a_sym);
+ } else if (outline_width < 0.0) {
+ // Ring min -> erode the strokes thinner
+ float core = 1.0;
+ core = min(core, sym_alpha(v_in.uv + float2( d.x, 0.0)));
+ core = min(core, sym_alpha(v_in.uv + float2(-d.x, 0.0)));
+ core = min(core, sym_alpha(v_in.uv + float2( 0.0, d.y)));
+ core = min(core, sym_alpha(v_in.uv + float2( 0.0, -d.y)));
+ core = min(core, sym_alpha(v_in.uv + float2( dg.x, dg.y)));
+ core = min(core, sym_alpha(v_in.uv + float2(-dg.x, dg.y)));
+ core = min(core, sym_alpha(v_in.uv + float2( dg.x, -dg.y)));
+ core = min(core, sym_alpha(v_in.uv + float2(-dg.x, -dg.y)));
+ a_sym = min(a_sym, core);
+ }
+
+ float a_sym_o = a_sym * opacity;
+ float a_out_o = a_out * opacity;
+
+ // Premultiplied: symbol color over an outline in its own color (x outline brightness)
+ float3 rgb = float3(hud_r, hud_g, hud_b) * a_sym_o
+ + float3(outline_r, outline_g, outline_b) * outline_brightness * a_out_o;
+ return float4(rgb, a_sym_o + a_out_o);
+}
+]==]
+
+------------------------------------------------------------
+-- Utility functions
+------------------------------------------------------------
+
+-- Matches the exact base name or an OBS auto-numbered duplicate (" 2", " 3", ...)
+local function source_name_matches(name, base)
+ if name == base then return true end
+ return name:sub(1, #base) == base and name:sub(#base + 1):match("^%s*%d+$") ~= nil
+end
+
+-- Finds a source by base name, falling back to auto-numbered duplicates.
+-- Returns nil or a reference the caller must release.
+local function get_source_by_base_name(base)
+ local exact = obs.obs_get_source_by_name(base)
+ if exact ~= nil then return exact end
+ local found = nil
+ local sources = obs.obs_enum_sources()
+ if sources ~= nil then
+ for _, s in ipairs(sources) do
+ if source_name_matches(obs.obs_source_get_name(s), base) then
+ found = obs.obs_source_get_ref(s)
+ break
+ end
+ end
+ obs.source_list_release(sources)
+ end
+ return found
+end
+
+-- Removes every source matching the base name, including duplicates left over
+-- from earlier runs. The scene items are removed first so the sources are
+-- destroyed immediately; otherwise OBS keeps them alive under the same name
+-- and renames newly created sources to " 2", " 3", ...
+local function remove_sources_by_base_name(scene, base)
+ local items = obs.obs_scene_enum_items(scene)
+ for _, item in ipairs(items) do
+ local s = obs.obs_sceneitem_get_source(item)
+ if s ~= nil and source_name_matches(obs.obs_source_get_name(s), base) then
+ obs.obs_sceneitem_remove(item)
+ end
+ end
+ obs.sceneitem_list_release(items)
+
+ local sources = obs.obs_enum_sources()
+ if sources ~= nil then
+ for _, s in ipairs(sources) do
+ if source_name_matches(obs.obs_source_get_name(s), base) then
+ obs.obs_source_remove(s)
+ end
+ end
+ obs.source_list_release(sources)
+ 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 preset = COLOR_PRESETS[color_index] or COLOR_PRESETS[1]
+ local outline_preset = OUTLINE_COLOR_PRESETS[outline_color_index] or OUTLINE_COLOR_PRESETS[1]
+
+ 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, "hud_r", preset.r * symbol_brightness)
+ obs.obs_data_set_double(fs, "hud_g", preset.g * symbol_brightness)
+ obs.obs_data_set_double(fs, "hud_b", preset.b * symbol_brightness)
+ obs.obs_data_set_double(fs, "outline_r", outline_preset.r)
+ obs.obs_data_set_double(fs, "outline_g", outline_preset.g)
+ obs.obs_data_set_double(fs, "outline_b", outline_preset.b)
+ 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_sources_by_base_name(scene, FRONT_NAME)
+ remove_sources_by_base_name(scene, 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 = get_source_by_base_name(BACK_NAME)
+ local front = get_source_by_base_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 and draws them in a configurable color and brightness,\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" ..
+ "\"Symbol color\" picks the hue, \"Symbol brightness\" scales it (0.35 = dark green, 1.0 = full brightness).\n" ..
+ "\"Symbol width\" thickens (positive) or thins (negative) the strokes; 0 keeps the original width.\n" ..
+ "\"Outline color\" picks the outline hue (default blue); the outline only appears when \"Symbol width\" is positive.\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, color, keying parameters).\n" ..
+ "The filter panel has sliders for fine-tuning (symbol color R/G/B, outline color R/G/B, 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 (*.*)", "")
+
+ local clist = obs.obs_properties_add_list(props, SETTING_COLOR,
+ "Symbol color", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_INT)
+ for i, c in ipairs(COLOR_PRESETS) do
+ obs.obs_property_list_add_int(clist, c.name, i)
+ end
+
+ obs.obs_properties_add_float_slider(props, SETTING_BRIGHTNESS,
+ "Symbol brightness (scales the chosen color)", 0.05, 1.0, 0.01)
+ 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,
+ "Symbol width (px, + thicker / - thinner, 0 = original)", -4.0, 8.0, 0.5)
+ obs.obs_properties_add_float_slider(props, SETTING_OUT_ALPHA,
+ "Outline strength", 0.0, 1.0, 0.01)
+
+ local olist = obs.obs_properties_add_list(props, SETTING_OUT_COLOR,
+ "Outline color (only visible when Symbol width > 0)", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_INT)
+ for i, c in ipairs(OUTLINE_COLOR_PRESETS) do
+ obs.obs_property_list_add_int(olist, c.name, i)
+ end
+
+ 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_int(settings, SETTING_COLOR, 1)
+ obs.obs_data_set_default_double(settings, SETTING_BRIGHTNESS, 1.0)
+ 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, 0.0)
+ obs.obs_data_set_default_double(settings, SETTING_OUT_ALPHA, 0.9)
+ obs.obs_data_set_default_int(settings, SETTING_OUT_COLOR, 1)
+ 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)
+ color_index = obs.obs_data_get_int(settings, SETTING_COLOR)
+ outline_color_index = obs.obs_data_get_int(settings, SETTING_OUT_COLOR)
+ symbol_brightness = obs.obs_data_get_double(settings, SETTING_BRIGHTNESS)
+ 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