推送Ai漏掉的文件

This commit is contained in:
evan.liu 2026-10-03 10:12:38 +08:00
parent 12764a1a22
commit 054bef361c
3 changed files with 1093 additions and 29 deletions

View File

@ -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

View File

@ -1,12 +1,13 @@
-- HUD Overlay Outline Script for OBS (separate Create / Update buttons) -- 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 -- New approach: detect the green symbols first, then draw them in a configurable
-- dark green outline, so they stay readable over ANY background (bright sky, green terrain, clouds). -- color (preset list x brightness), so they stay readable over ANY background
-- (bright sky, green terrain, clouds).
-- Principle: -- Principle:
-- 1. symbol detection: subtract a black level, alpha = max(R, G, B) x gain, -- 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 -- 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 -- 2. optional outline: sample the symbol alpha in a ring around each pixel; pixels
-- near a symbol but not part of it get a dark outline underneath the green fill -- near a symbol get the same color (x outline brightness) underneath the fill
-- 3. premultiplied output: pure green x symbol alpha + outline color x outline alpha -- 3. premultiplied output: symbol color x symbol alpha + outline color x outline alpha
-- Requires: obs-shaderfilter plugin (installed) -- Requires: obs-shaderfilter plugin (installed)
-- Install: OBS -> Tools -> Scripts -> + -> select this file -- Install: OBS -> Tools -> Scripts -> + -> select this file
-- "Create Scene" : builds the scene from scratch (deletes and recreates both media sources) -- "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_FRONT_PATH = "front_path"
local SETTING_BACK_PATH = "back_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_BLACK_LEVEL = "black_level"
local SETTING_GAIN = "alpha_gain" local SETTING_GAIN = "alpha_gain"
local SETTING_GREEN_THRESH = "green_thresh" local SETTING_GREEN_THRESH = "green_thresh"
@ -28,12 +31,27 @@ local FRONT_NAME = "HUD - Front Outline"
local BACK_NAME = "Center IG - Back Outline" local BACK_NAME = "Center IG - Back Outline"
local FILTER_NAME = "HUD 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 front_path = ""
local back_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 black_level = 0.055 -- 14/255, measured noise floor of the HUD capture
local alpha_gain = 1.35 local alpha_gain = 1.35
local green_thresh = 0.10 -- greenness (G - max(R,B)) needed to count as a green symbol 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 outline_alpha = 0.9
local opacity = 1.0 local opacity = 1.0
@ -68,12 +86,12 @@ uniform float green_thresh <
> = 0.10; > = 0.10;
uniform float outline_width < uniform float outline_width <
string label = "Outline width (px)"; string label = "Symbol width (px, + thicker / - thinner)";
string widget_type = "slider"; string widget_type = "slider";
float minimum = 0.0; float minimum = -4.0;
float maximum = 8.0; float maximum = 8.0;
float step = 0.5; float step = 0.5;
> = 2.0; > = 0.0;
uniform float outline_alpha < uniform float outline_alpha <
string label = "Outline strength"; string label = "Outline strength";
@ -113,7 +131,7 @@ uniform float outline_brightness <
float minimum = 0.0; float minimum = 0.0;
float maximum = 1.0; float maximum = 1.0;
float step = 0.01; float step = 0.01;
> = 0.35; > = 1.0;
uniform float opacity < uniform float opacity <
string label = "Opacity"; string label = "Opacity";
@ -146,9 +164,13 @@ float4 mainImage(VertData v_in) : TARGET
{ {
float a_sym = sym_alpha(v_in.uv); float a_sym = sym_alpha(v_in.uv);
// Ring of 8 neighbor taps at outline_width distance -> dilated symbol alpha // Symbol width: positive = dilate (thicker strokes), negative = erode (thinner)
float2 d = uv_pixel_interval * outline_width; float2 d = uv_pixel_interval * abs(outline_width);
float2 dg = d * 0.7071; 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; 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(-d.x, 0.0))); ring = max(ring, sym_alpha(v_in.uv + float2(-d.x, 0.0)));
@ -158,14 +180,26 @@ float4 mainImage(VertData v_in) : TARGET
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)));
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 // Outline only where the symbol itself is not already opaque
float a_out = min(ring * outline_alpha, 1.0 - a_sym); 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_sym_o = a_sym * opacity;
float a_out_o = a_out * 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 rgb = float3(hud_r, hud_g, hud_b) * a_sym_o
+ float3(hud_r, hud_g, hud_b) * outline_brightness * a_out_o; + float3(hud_r, hud_g, hud_b) * outline_brightness * a_out_o;
return float4(rgb, a_sym_o + 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) obs.obs_source_release(old)
end end
local preset = COLOR_PRESETS[color_index] or COLOR_PRESETS[1]
local fs = obs.obs_data_create() local fs = obs.obs_data_create()
obs.obs_data_set_bool(fs, "from_file", false) obs.obs_data_set_bool(fs, "from_file", false)
obs.obs_data_set_string(fs, "shader_text", SHADER_TEXT) 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, "black_level", black_level)
obs.obs_data_set_double(fs, "alpha_gain", alpha_gain) 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, "green_thresh", green_thresh)
@ -395,12 +434,14 @@ end
------------------------------------------------------------ ------------------------------------------------------------
function script_description() 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" .. "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 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" .. "\"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" .. "\"Update Scene\": updates the existing scene in place (video paths, color, keying parameters).\n" ..
"The filter panel has sliders for symbol color, outline width, strength and brightness." "The filter panel has sliders for fine-tuning (symbol color R/G/B, outline width, strength and brightness)."
end end
function script_properties() function script_properties()
@ -414,6 +455,14 @@ function script_properties()
"Background video (bottom layer)", obs.OBS_PATH_FILE, "Background video (bottom layer)", obs.OBS_PATH_FILE,
"Video files (*.avi *.mp4 *.mov *.mkv *.webm);;All files (*.*)", "") "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, 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) "Black level (increase if a dark halo/noise remains)", 0.0, 0.3, 0.001)
obs.obs_properties_add_float_slider(props, SETTING_GAIN, 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, obs.obs_properties_add_float_slider(props, SETTING_GREEN_THRESH,
"Green threshold (increase if non-green noise appears)", 0.01, 0.5, 0.005) "Green threshold (increase if non-green noise appears)", 0.01, 0.5, 0.005)
obs.obs_properties_add_float_slider(props, SETTING_OUT_WIDTH, 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, obs.obs_properties_add_float_slider(props, SETTING_OUT_ALPHA,
"Outline strength", 0.0, 1.0, 0.01) "Outline strength", 0.0, 1.0, 0.01)
obs.obs_properties_add_float_slider(props, SETTING_OPACITY, 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") "D:\\Test Pictures + Video\\HUD - Front Video.avi")
obs.obs_data_set_default_string(settings, SETTING_BACK_PATH, obs.obs_data_set_default_string(settings, SETTING_BACK_PATH,
"D:\\Test Pictures + Video\\Center IG - Back Videos.avi") "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_BLACK_LEVEL, 0.055)
obs.obs_data_set_default_double(settings, SETTING_GAIN, 1.35) 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_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_OUT_ALPHA, 0.9)
obs.obs_data_set_default_double(settings, SETTING_OPACITY, 1.0) obs.obs_data_set_default_double(settings, SETTING_OPACITY, 1.0)
end end
@ -449,6 +500,8 @@ end
function script_update(settings) function script_update(settings)
front_path = obs.obs_data_get_string(settings, SETTING_FRONT_PATH) front_path = obs.obs_data_get_string(settings, SETTING_FRONT_PATH)
back_path = obs.obs_data_get_string(settings, SETTING_BACK_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) black_level = obs.obs_data_get_double(settings, SETTING_BLACK_LEVEL)
alpha_gain = obs.obs_data_get_double(settings, SETTING_GAIN) alpha_gain = obs.obs_data_get_double(settings, SETTING_GAIN)
green_thresh = obs.obs_data_get_double(settings, SETTING_GREEN_THRESH) green_thresh = obs.obs_data_get_double(settings, SETTING_GREEN_THRESH)

View File

@ -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 ("<base> 2", "<base> 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 "<name> 2", "<name> 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