Skip to content

Commit e80a1e4

Browse files
committed
Add glowing shape cursor, update readme
1 parent 4dec6f9 commit e80a1e4

File tree

3 files changed

+475
-3
lines changed

3 files changed

+475
-3
lines changed

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
# OBS Studio Cursor skin
2-
Selected source will follow mouse pointer.
3-
Using [`obs_sceneitem_set_pos`](https://obsproject.com/docs/reference-scenes.html#c.obs_sceneitem_set_pos)
2+
This script pack adds various cursors to be rendered inside OBS Studio.
3+
There are two main variations of scripts. The Python version uses common API's and requires installation of 3-rd party
4+
pip packages, this version is crossplatform. The Lua version uses shaders and is not crossplatform, currently Windows only.
5+
6+
# Lua and HLSL shaders cursor
7+
## Installation
8+
Move files to some permanent location, select and add .lua files to OBS Studio.
9+
Add a filter to a source e.g Desktop Capture.
10+
11+
## Preview 1440p resolution in fullscreen game.
12+
13+
- Neon shape - you can customize color and size.
14+
15+
![img](https://i.imgur.com/KPWO3id.png)
16+
17+
- 3D magic stick - as is.
18+
19+
![img](https://i.imgur.com/kZEfxkd.png)
20+
21+
# Python version
22+
423
# Installation
524
- Install pynput package from [pypi](https://pypi.org/project/pynput/)
625
- Make sure your OBS Studio supports [scripting](https://obsproject.com/docs/scripting.html)
726
`python -m pip install pynput`
8-
- `cursor_shader.lua` - Standalone shader based Windows only 3D cursor skin.
927
# Limitations
1028
- Multiple monitors setup currently not working .
1129
- If used in fullscreen apps, offset might appear.
@@ -60,6 +78,7 @@ They all have some level of transparency.
6078
- [`3_4_700`](https://github.com/34700) - added offsets functionality for precise custom cursor(like a hand drawn arm holding a pen for artists)
6179
- [`tholman/cursor-effects`](https://github.com/tholman/cursor-effects) - stock cursor trails
6280
- [`inspirnathan`](https://github.com/inspirnathan) - SDF shaders implementation tutorial [series](https://inspirnathan.com/posts/53-shadertoy-tutorial-part-7/)
81+
- [`bfxdev/OBS`](https://github.com/bfxdev/OBS) - Shader tutorials and code specific for OBS Studio.
6382

6483
# Contribute
6584
You are welcome to contribute. Help is needed.

cursor_sha_der_glow1.lua

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
S = obslua
2+
local ffi = require"ffi"
3+
local C = ffi.C
4+
local _OR = bit.bor
5+
local x11,mouse_pos,get_x11_mpos;
6+
if ffi.os == "Windows" then
7+
ffi.cdef[[
8+
typedef struct {int x, y;} Point;
9+
bool GetCursorPos(Point *lpPoint);
10+
]]
11+
mouse_pos = ffi.new("Point")
12+
elseif ffi.os == "Linux" then
13+
mouse_pos = {x=0,y=0}
14+
x11 = ffi.load("X11.so.6")
15+
ffi.cdef[[
16+
typedef void Display;
17+
typedef unsigned long XID;
18+
typedef XID Window;
19+
typedef XID Colormap;
20+
typedef struct{
21+
void* ext_data;
22+
void* display;
23+
Window root;
24+
int width, height;
25+
int mwidth, mheight;
26+
int ndepths;
27+
void* depths;
28+
int root_depth;
29+
void* root_visual;
30+
void* default_gc;
31+
Colormap cmap;
32+
// Rest doesn't matter
33+
}Screen;
34+
typedef char* XPointer;
35+
typedef struct{
36+
void* ext_data;
37+
void* private1;
38+
int fd;
39+
int private2;
40+
int proto_major_version;
41+
int proto_minor_version;
42+
char* vendor;
43+
XID private3;
44+
XID private4;
45+
XID private5;
46+
int private6;
47+
XID (*resource_alloc)(void*);
48+
int byte_order;
49+
int bitmap_unit;
50+
int bitmap_pad;
51+
int bitmap_bit_order;
52+
int nformats;
53+
void* pixmap_format;
54+
int private8;
55+
int release;
56+
void* private9, *private10;
57+
int qlen;
58+
unsigned long last_request_read;
59+
unsigned long request;
60+
XPointer private11;
61+
XPointer private12;
62+
XPointer private13;
63+
XPointer private14;
64+
unsigned max_request_size;
65+
void* db;
66+
int (*private15)(void*);
67+
char* display_name;
68+
int default_screen;
69+
// Rest doesn't matter
70+
}*_XPrivDisplay;
71+
typedef int Bool;
72+
typedef struct{
73+
int x, y;
74+
int width, height;
75+
int border_width;
76+
int depth;
77+
void* visual;
78+
Window root;
79+
int class;
80+
int bit_gravity;
81+
int win_gravity;
82+
int backing_store;
83+
unsigned long backing_planes;
84+
unsigned long backing_pixel;
85+
Bool save_under;
86+
Colormap colormap;
87+
Bool map_installed;
88+
int map_state;
89+
long all_event_masks;
90+
long your_event_mask;
91+
long do_not_propagate_mask;
92+
Bool override_redirect;
93+
Screen *screen;
94+
}XWindowAttributes;
95+
Display* XOpenDisplay(char*);
96+
int XCloseDisplay(Display*);
97+
Screen* XScreenOfDisplay(Display*, int);
98+
Bool XQueryPointer(Display*, Window, Window*, Window*, int*, int*, int*, int*, unsigned int*);
99+
]]
100+
-- from https://gist.github.com/Youka/193afdec83321f4f51a2
101+
function get_x11_mpos()
102+
local display = x11.XOpenDisplay(nil)
103+
if not display then
104+
error("Couldn't open display!", 2)
105+
end
106+
local root = x11.XScreenOfDisplay(display, ffi.cast("_XPrivDisplay", display)[0].default_screen)[0].root
107+
-- Get cursor position
108+
local root_window, child_window, root_x, root_y, win_x, win_y, mask = ffi.new("Window[1]"), ffi.new("Window[1]"), ffi.new("int[1]"), ffi.new("int[1]"), ffi.new("int[1]"), ffi.new("int[1]"), ffi.new("unsigned int[1]")
109+
if x11.XQueryPointer(display, root, root_window, child_window, root_x, root_y, win_x, win_y, mask) == 0 then
110+
error("Couldn't get cursor position!", 2)
111+
end
112+
x11.XCloseDisplay(display)
113+
return win_x[0], win_y[0]
114+
end
115+
116+
else
117+
error("Not implemented")
118+
end
119+
_G.pressed_l, _G.pressed_r = false, false
120+
121+
local function skip_tick_render(ctx)
122+
local target = S.obs_filter_get_target(ctx.source)
123+
local width, height;
124+
if target == nil then width = 0; height = 0; else
125+
width = S.obs_source_get_base_width(target)
126+
height = S.obs_source_get_base_height(target)
127+
end
128+
ctx.width, ctx.height = width , height
129+
end
130+
131+
local function hook_mouse_buttons()
132+
if _G.MOUSE_HOOKED then return end
133+
local key_1 = '{"htk_1_mouse": [ { "key": "OBS_KEY_MOUSE1" } ], '
134+
local key_2 = '"htk_2_mouse": [ { "key": "OBS_KEY_MOUSE2" } ]}'
135+
local json_s = key_1 .. key_2
136+
default_hotkeys = {
137+
{id="htk_1_mouse", des="LMB state", callback=function(p) _G.pressed_l = p end},
138+
{id="htk_2_mouse", des="RMB state", callback=function(p) _G.pressed_r = p end},
139+
}
140+
local settings = S.obs_data_create_from_json(json_s)
141+
for _,k in pairs(default_hotkeys) do
142+
local a = S.obs_data_get_array(settings, k.id)
143+
local h = S.obs_hotkey_register_frontend(k.id, k.des, k.callback)
144+
S.obs_hotkey_load(h, a)
145+
S.obs_data_array_release(a)
146+
end
147+
S.obs_data_release(settings)
148+
_G.MOUSE_HOOKED = True
149+
end
150+
151+
local SourceDef = {}
152+
153+
function SourceDef:new(o)
154+
o = o or {}
155+
setmetatable(o, self)
156+
self.__index = self
157+
return o
158+
end
159+
160+
function SourceDef:create(source)
161+
local instance = {}
162+
instance.width = 1
163+
instance.height = 1
164+
instance.current_time = 0
165+
instance.source = source
166+
S.obs_enter_graphics()
167+
instance.effect = S.gs_effect_create(SHADER, nil, nil)
168+
if instance.effect ~= nil then
169+
instance.params = {}
170+
instance.params.width = S.gs_effect_get_param_by_name(instance.effect, 'width')
171+
instance.params.itime = S.gs_effect_get_param_by_name(instance.effect, 'itime')
172+
instance.params.height = S.gs_effect_get_param_by_name(instance.effect, 'height')
173+
instance.params.mouse_x = S.gs_effect_get_param_by_name(instance.effect, 'mouse_x')
174+
instance.params.mouse_y = S.gs_effect_get_param_by_name(instance.effect, 'mouse_y')
175+
instance.params.image = S.gs_effect_get_param_by_name(instance.effect, 'image')
176+
instance.params.pressed_l = S.gs_effect_get_param_by_name(instance.effect, 'pressed_l')
177+
instance.params.pressed_r = S.gs_effect_get_param_by_name(instance.effect, 'pressed_r')
178+
instance.params.idsd = S.gs_effect_get_param_by_name(instance.effect, 'idsd')
179+
instance.params.rsize = S.gs_effect_get_param_by_name(instance.effect, 'rsize')
180+
instance.params.r1 = S.gs_effect_get_param_by_name(instance.effect, 'r1')
181+
instance.params.g1 = S.gs_effect_get_param_by_name(instance.effect, 'g1')
182+
instance.params.b1 = S.gs_effect_get_param_by_name(instance.effect, 'b1')
183+
instance.params.r2 = S.gs_effect_get_param_by_name(instance.effect, 'r2')
184+
instance.params.g2 = S.gs_effect_get_param_by_name(instance.effect, 'g2')
185+
instance.params.b2 = S.gs_effect_get_param_by_name(instance.effect, 'b2')
186+
end
187+
S.obs_leave_graphics()
188+
if instance.effect == nil then
189+
SourceDef.destroy(instance)
190+
return nil
191+
end
192+
SourceDef.update(instance,self) -- initialize, self = settings
193+
return instance
194+
end
195+
196+
function SourceDef:destroy()
197+
if self.effect ~= nil then
198+
S.obs_enter_graphics()
199+
S.gs_effect_destroy(self.effect)
200+
S.obs_leave_graphics()
201+
end
202+
end
203+
204+
function SourceDef:get_name() return "Neon shape cursor by upgradeQ" end
205+
206+
function SourceDef:get_properties()
207+
local props = S.obs_properties_create()
208+
S.obs_properties_add_float_slider(props, "idsd", "Shape id", 0.5, 3.0, 1.00)
209+
S.obs_properties_add_float_slider(props, "rsize", "Size", 0.01, 0.3, 0.0001)
210+
S.obs_properties_add_float_slider(props, "r1", "Red channel", 0.0, 1.0, 0.0001)
211+
S.obs_properties_add_float_slider(props, "g1", "Green channel", 0.0, 1.0, 0.0001)
212+
S.obs_properties_add_float_slider(props, "b1", "Blue channel", 0.0, 1.0, 0.0001)
213+
S.obs_properties_add_float_slider(props, "r2", "[i]Red channel", 0.0, 1.0, 0.0001)
214+
S.obs_properties_add_float_slider(props, "g2", "[i]Green channel", 0.0, 1.0, 0.0001)
215+
S.obs_properties_add_float_slider(props, "b2", "[i]Blue channel", 0.0, 1.0, 0.0001)
216+
return props
217+
end
218+
219+
function SourceDef.get_defaults(settings)
220+
S.obs_data_set_default_double(settings, "rsize", 0.05)
221+
S.obs_data_set_default_double(settings, "idsd", 0.0)
222+
S.obs_data_set_default_double(settings, "r1", 0.0)
223+
S.obs_data_set_default_double(settings, "g1", 0.5)
224+
S.obs_data_set_default_double(settings, "b1", 0.0)
225+
S.obs_data_set_default_double(settings, "r2", 1.0)
226+
S.obs_data_set_default_double(settings, "g2", 1.0)
227+
S.obs_data_set_default_double(settings, "b2", 1.0)
228+
end
229+
230+
function SourceDef:update(settings)
231+
self.rsize = S.obs_data_get_double(settings, "rsize")
232+
self.idsd = S.obs_data_get_double(settings, "idsd")
233+
self.r1 = S.obs_data_get_double(settings, "r1")
234+
self.g1 = S.obs_data_get_double(settings, "g1")
235+
self.b1 = S.obs_data_get_double(settings, "b1")
236+
self.r2 = S.obs_data_get_double(settings, "r2")
237+
self.g2 = S.obs_data_get_double(settings, "g2")
238+
self.b2 = S.obs_data_get_double(settings, "b2")
239+
end
240+
function SourceDef:get_width() return self.width end
241+
function SourceDef:get_height() return self.height end
242+
function SourceDef:video_tick(seconds)
243+
self.current_time = self.current_time + seconds
244+
skip_tick_render(self) -- if source has crop or transform applied to it, this will let it render
245+
end
246+
247+
local function norm(val,amin,amax) return (val-amin ) / (amax- amin) end
248+
249+
function SourceDef:video_render()
250+
if ffi.os == "Windows" then
251+
C.GetCursorPos(mouse_pos)
252+
else
253+
mouse_pos.x,mouse_pos.y = get_x11_mpos()
254+
end
255+
local target = S.obs_filter_get_target(self.source)
256+
local norm_x = norm(mouse_pos.x,0,self.width)
257+
local norm_y = norm(mouse_pos.y,0,self.height)
258+
S.gs_effect_set_float(self.params.itime, self.current_time+0.0)
259+
S.gs_effect_set_float(self.params.mouse_x, norm_x)
260+
S.gs_effect_set_float(self.params.mouse_y, norm_y)
261+
S.gs_effect_set_bool(self.params.pressed_l, _G.pressed_l)
262+
S.gs_effect_set_bool(self.params.pressed_r, _G.pressed_r)
263+
S.gs_effect_set_int(self.params.width, self.width)
264+
S.gs_effect_set_int(self.params.height, self.height)
265+
S.gs_effect_set_float(self.params.rsize, self.rsize)
266+
S.gs_effect_set_float(self.params.idsd, self.idsd)
267+
S.gs_effect_set_float(self.params.r1, self.r1)
268+
S.gs_effect_set_float(self.params.g1, self.g1)
269+
S.gs_effect_set_float(self.params.b1, self.b1)
270+
S.gs_effect_set_float(self.params.r2, self.r2)
271+
S.gs_effect_set_float(self.params.g2, self.g2)
272+
S.gs_effect_set_float(self.params.b2, self.b2)
273+
if not S.obs_source_process_filter_begin(self.source, S.GS_RGBA, S.OBS_ALLOW_DIRECT_RENDERING) then return end
274+
S.obs_source_process_filter_tech_end(self.source, self.effect, self.width, self.height,"Draw")
275+
end
276+
277+
function script_properties()
278+
local props = S.obs_properties_create()
279+
S.obs_properties_add_button(props, "button2", "Neon shape cursor by upgradeQ",
280+
function() end)
281+
return props
282+
end
283+
284+
function script_load(settings) -- OBS_SOURCE_CUSTOM_DRAW
285+
local my_filter = SourceDef:new({id='filter_cursor_shader_neon',type=S.OBS_SOURCE_TYPE_FILTER,
286+
output_flags=_OR(S.OBS_SOURCE_VIDEO,S.OBS_SOURCE_CUSTOM_DRAW)})
287+
hook_mouse_buttons()
288+
S.obs_register_source(my_filter)
289+
end
290+
291+
function read_from(file)
292+
local f = assert(io.open(file, "rb"))
293+
local content = f:read("*all")
294+
f:close()
295+
return content
296+
end
297+
298+
--SHADER = ([==[ ]==])
299+
SHADER = read_from(script_path() .. "my_glow_shader.hlsl")

0 commit comments

Comments
 (0)