diff --git a/data/effects/transform.effect b/data/effects/transform.effect new file mode 100644 index 00000000..85c01608 --- /dev/null +++ b/data/effects/transform.effect @@ -0,0 +1,122 @@ +#include "shared.effect" + +uniform texture2D InputA< + bool automatic = true; +>; + +uniform float2 CornerTL< + string name = "Corner: Top Left"; + string field_type = "slider"; + string suffix = " %"; + float2 minimum = {-100., -100.}; + float2 maximum = {-200., -200.}; + float2 step = {.01, .01}; + float2 scale = {.01, .01}; +> = {0., 0.}; + +uniform float2 CornerTR< + string name = "Corner: Top Right"; + string field_type = "slider"; + string suffix = " %"; + float2 minimum = {-100., -100.}; + float2 maximum = {-200., -200.}; + float2 step = {.01, .01}; + float2 scale = {.01, .01}; +> = {100., 0.}; + +uniform float2 CornerBL< + string name = "Corner: Bottom Left"; + string field_type = "slider"; + string suffix = " %"; + float2 minimum = {-100., -100.}; + float2 maximum = {-200., -200.}; + float2 step = {.01, .01}; + float2 scale = {.01, .01}; +> = {0., 100.}; + +uniform float2 CornerBR< + string name = "Corner: Bottom Right"; + string field_type = "slider"; + string suffix = " %"; + float2 minimum = {-100., -100.}; + float2 maximum = {-200., -200.}; + float2 step = {.01, .01}; + float2 scale = {.01, .01}; +> = {100., 100.}; + +//------------------------------------------------------------------------------ +// Technique: Corner Pin +//------------------------------------------------------------------------------ +// +// Credits: +// - Inigo Quilez: https://www.iquilezles.org/www/articles/ibilinear/ibilinear.htm +// +// Parameters: +// - InputA: RGBA Texture +// - CornerTL: Corner "A" +// - CornerTR: Corner "B" +// - CornerBL: Corner "D" +// - CornerBR: Corner "C" + +float2 cross2d(in float2 a, in float2 b) { + return (a.x * b.y) - (a.y * b.x); +}; + +float2 inverse_bilinear(in float2 p, in float2 a, in float2 b, in float2 c, in float2 d) { + float2 result = float2(-1., -1.); + + float2 e = b - a; + float2 f = d - a; + float2 g = a-b+c-d; + float2 h = p-a; + + float k2 = cross2d(g, f); + float k1 = cross2d(e, f) + cross2d(h, g); + float k0 = cross2d(h, e); + + if (abs(k2) < .001) { // Edges are likely parallel, so this is a linear equation. + result = float2( + (h.x * k1 + f.x * k0) / (e.x * k1 - g.x * k0), + -k0 / k1 + ); + } else { // It's a quadratic equation. + float w = k1 * k1 - 4.0 * k0 * k2; + if (w < 0.0) { // Prevent GPUs from going insane. + return result; + } + w = sqrt(w); + + float ik2 = 0.5/k2; + float v = (-k1 - w) * ik2; + float u = (h.x - f.x * v) / (e.x + g.x * v); + + if (u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0) { + v = (-k1 + w) * ik2; + u = (h.x - f.x * v) / (e.x + g.x * v); + } + + result = float2(u, v); + } + + return result; +}; + +float4 PSCornerPin(VertexData vtx) : TARGET { + // Convert from screen coords to potential Quad UV coordinates. + float2 uv = inverse_bilinear((vtx.uv * 2.) - 1., CornerTL, CornerTR, CornerBR, CornerBL); + + if (max(abs(uv.x - .5), abs(uv.y - .5)) >= .5) { + return float4(0, 0, 0, 0); + } + + return InputA.Sample(BlankSampler, uv); +}; + +technique CornerPin +{ + pass + { + vertex_shader = DefaultVertexShader(vtx); + pixel_shader = PSCornerPin(vtx); + }; +}; diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index d58087b4..40479019 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -398,6 +398,7 @@ Filter.SDFEffects.SDF.Threshold="SDF Alpha Threshold" Filter.Transform="3D Transform" Filter.Transform.Camera="Camera" Filter.Transform.Camera.Mode="Mode" +Filter.Transform.Camera.Mode.CornerPin="Corner Pin" Filter.Transform.Camera.Mode.Orthographic="Orthographic" Filter.Transform.Camera.Mode.Perspective="Perspective" Filter.Transform.Camera.FieldOfView="Field Of View" @@ -422,6 +423,11 @@ Filter.Transform.Rotation.Order.YXZ="Yaw, Pitch, Roll" Filter.Transform.Rotation.Order.YZX="Yaw, Roll, Pitch" Filter.Transform.Rotation.Order.ZXY="Roll, Pitch, Yaw" Filter.Transform.Rotation.Order.ZYX="Roll, Yaw, Pitch" +Filter.Transform.Corners="Corners" +Filter.Transform.Corners.TopLeft="Top Left" +Filter.Transform.Corners.TopRight="Top Right" +Filter.Transform.Corners.BottomLeft="Bottom Left" +Filter.Transform.Corners.BottomRight="Bottom Right" Filter.Transform.Mipmapping="Enable Mipmapping" # Filter - Upscaling diff --git a/source/filters/filter-transform.cpp b/source/filters/filter-transform.cpp index 1b0ccbaf..14935c9b 100644 --- a/source/filters/filter-transform.cpp +++ b/source/filters/filter-transform.cpp @@ -54,6 +54,7 @@ #define ST_I18N_CAMERA ST_I18N ".Camera" #define ST_I18N_CAMERA_MODE ST_I18N_CAMERA ".Mode" #define ST_KEY_CAMERA_MODE "Camera.Mode" +#define ST_I18N_CAMERA_MODE_CORNER_PIN ST_I18N_CAMERA_MODE ".CornerPin" #define ST_I18N_CAMERA_MODE_ORTHOGRAPHIC ST_I18N_CAMERA_MODE ".Orthographic" #define ST_I18N_CAMERA_MODE_PERSPECTIVE ST_I18N_CAMERA_MODE ".Perspective" #define ST_I18N_CAMERA_FIELDOFVIEW ST_I18N_CAMERA ".FieldOfView" @@ -80,6 +81,15 @@ #define ST_I18N_ROTATION_ORDER_YZX ST_I18N_ROTATION_ORDER ".YZX" #define ST_I18N_ROTATION_ORDER_ZXY ST_I18N_ROTATION_ORDER ".ZXY" #define ST_I18N_ROTATION_ORDER_ZYX ST_I18N_ROTATION_ORDER ".ZYX" +#define ST_I18N_CORNERS ST_I18N ".Corners" +#define ST_I18N_CORNERS_TOPLEFT ST_I18N_CORNERS ".TopLeft" +#define ST_KEY_CORNERS_TOPLEFT "Corners.TopLeft." +#define ST_I18N_CORNERS_TOPRIGHT ST_I18N_CORNERS ".TopRight" +#define ST_KEY_CORNERS_TOPRIGHT "Corners.TopRight." +#define ST_I18N_CORNERS_BOTTOMLEFT ST_I18N_CORNERS ".BottomLeft" +#define ST_KEY_CORNERS_BOTTOMLEFT "Corners.BottomLeft." +#define ST_I18N_CORNERS_BOTTOMRIGHT ST_I18N_CORNERS ".BottomRight" +#define ST_KEY_CORNERS_BOTTOMRIGHT "Corners.BottomRight." #define ST_I18N_MIPMAPPING ST_I18N ".Mipmapping" #define ST_KEY_MIPMAPPING "Mipmapping" @@ -100,8 +110,9 @@ enum RotationOrder : int64_t { }; transform_instance::transform_instance(obs_data_t* data, obs_source_t* context) - : obs::source_instance(data, context), _camera_mode(), _camera_fov(), _standard_effect(), _sampler(), _params(), - _cache_rendered(), _mipmap_enabled(), _source_rendered(), _source_size(), _update_mesh(true) + : obs::source_instance(data, context), _camera_mode(), _camera_fov(), _standard_effect(), _transform_effect(), + _sampler(), _params(), _corners(), _cache_rendered(), _mipmap_enabled(), _source_rendered(), _source_size(), + _update_mesh(true) { _cache_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); _source_rt = std::make_shared(GS_RGBA, GS_ZS_NONE); @@ -114,6 +125,14 @@ transform_instance::transform_instance(obs_data_t* data, obs_source_t* context) DLOG_ERROR("Error loading '%s' from disk: %s", file.generic_u8string().c_str(), ex.what()); } } + { + auto file = streamfx::data_file_path("effects/transform.effect"); + try { + _transform_effect = streamfx::obs::gs::effect::create(file.generic_u8string()); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s' from disk: %s", file.generic_u8string().c_str(), ex.what()); + } + } { _sampler.set_address_mode_u(GS_ADDRESS_CLAMP); _sampler.set_address_mode_v(GS_ADDRESS_CLAMP); @@ -126,6 +145,11 @@ transform_instance::transform_instance(obs_data_t* data, obs_source_t* context) vec3_set(&_params.scale, 1, 1, 1); vec3_set(&_params.shear, 0, 0, 0); + vec2_set(&_corners.tl, 0, 0); + vec2_set(&_corners.tr, 1, 0); + vec2_set(&_corners.bl, 0, 1); + vec2_set(&_corners.br, 1, 1); + update(data); } @@ -213,6 +237,17 @@ void transform_instance::update(obs_data_t* settings) _params.shear.y = static_cast(obs_data_get_double(settings, ST_KEY_SHEAR_Y) / 100.0); _params.shear.z = 0.0f; } + { // Corners + std::pair opts[] = { + {ST_KEY_CORNERS_TOPLEFT "X", _corners.tl.x}, {ST_KEY_CORNERS_TOPLEFT "Y", _corners.tl.y}, + {ST_KEY_CORNERS_TOPRIGHT "X", _corners.tr.x}, {ST_KEY_CORNERS_TOPRIGHT "Y", _corners.tr.y}, + {ST_KEY_CORNERS_BOTTOMLEFT "X", _corners.bl.x}, {ST_KEY_CORNERS_BOTTOMLEFT "Y", _corners.bl.y}, + {ST_KEY_CORNERS_BOTTOMRIGHT "X", _corners.br.x}, {ST_KEY_CORNERS_BOTTOMRIGHT "Y", _corners.br.y}, + }; + for (auto opt : opts) { + opt.second = static_cast(obs_data_get_double(settings, opt.first.c_str()) / 100.0); + } + } // Mip-mapping _mipmap_enabled = obs_data_get_bool(settings, ST_KEY_MIPMAPPING); @@ -252,82 +287,85 @@ void transform_instance::video_tick(float) height = 1; } - // Calculate Aspect Ratio - float aspect_ratio_x = float(width) / float(height); + if (_camera_mode != transform_mode::CORNER_PIN) { + // Calculate Aspect Ratio + float aspect_ratio_x = float(width) / float(height); + if (_camera_mode == transform_mode::ORTHOGRAPHIC) + aspect_ratio_x = 1.0; - if (_camera_mode == transform_mode::ORTHOGRAPHIC) - aspect_ratio_x = 1.0; + // Mesh + matrix4 ident; + matrix4_identity(&ident); + switch (_params.rotation_order) { + case RotationOrder::XYZ: // XYZ + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + break; + case RotationOrder::XZY: // XZY + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + break; + case RotationOrder::YXZ: // YXZ + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + break; + case RotationOrder::YZX: // YZX + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + break; + case RotationOrder::ZXY: // ZXY + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + break; + case RotationOrder::ZYX: // ZYX + matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); + matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); + matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); + break; + } + matrix4_translate3f(&ident, &ident, _params.position.x, _params.position.y, _params.position.z); + //matrix4_scale3f(&ident, &ident, _source_size.first / 2.f, _source_size.second / 2.f, 1.f); - // Mesh - matrix4 ident; - matrix4_identity(&ident); - switch (_params.rotation_order) { - case RotationOrder::XYZ: // XYZ - matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); - matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); - matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); - break; - case RotationOrder::XZY: // XZY - matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); - matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); - matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); - break; - case RotationOrder::YXZ: // YXZ - matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); - matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); - matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); - break; - case RotationOrder::YZX: // YZX - matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); - matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); - matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); - break; - case RotationOrder::ZXY: // ZXY - matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); - matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); - matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); - break; - case RotationOrder::ZYX: // ZYX - matrix4_rotate_aa4f(&ident, &ident, 0, 0, 1, _params.rotation.z); - matrix4_rotate_aa4f(&ident, &ident, 0, 1, 0, _params.rotation.y); - matrix4_rotate_aa4f(&ident, &ident, 1, 0, 0, _params.rotation.x); - break; - } - matrix4_translate3f(&ident, &ident, _params.position.x, _params.position.y, _params.position.z); - //matrix4_scale3f(&ident, &ident, _source_size.first / 2.f, _source_size.second / 2.f, 1.f); + /// Calculate vertex position once only. + float p_x = aspect_ratio_x * _params.scale.x; + float p_y = 1.0f * _params.scale.y; - /// Calculate vertex position once only. - float p_x = aspect_ratio_x * _params.scale.x; - float p_y = 1.0f * _params.scale.y; - - /// Generate mesh - { - auto vtx = _vertex_buffer->at(0); - *vtx.color = 0xFFFFFFFF; - vec4_set(vtx.uv[0], 0, 0, 0, 0); - vec3_set(vtx.position, -p_x + _params.shear.x, -p_y - _params.shear.y, 0); - vec3_transform(vtx.position, vtx.position, &ident); - } - { - auto vtx = _vertex_buffer->at(1); - *vtx.color = 0xFFFFFFFF; - vec4_set(vtx.uv[0], 1, 0, 0, 0); - vec3_set(vtx.position, p_x + _params.shear.x, -p_y + _params.shear.y, 0); - vec3_transform(vtx.position, vtx.position, &ident); - } - { - auto vtx = _vertex_buffer->at(2); - *vtx.color = 0xFFFFFFFF; - vec4_set(vtx.uv[0], 0, 1, 0, 0); - vec3_set(vtx.position, -p_x - _params.shear.x, p_y - _params.shear.y, 0); - vec3_transform(vtx.position, vtx.position, &ident); - } - { - auto vtx = _vertex_buffer->at(3); - *vtx.color = 0xFFFFFFFF; - vec4_set(vtx.uv[0], 1, 1, 0, 0); - vec3_set(vtx.position, p_x - _params.shear.x, p_y + _params.shear.y, 0); - vec3_transform(vtx.position, vtx.position, &ident); + /// Generate mesh + { + auto vtx = _vertex_buffer->at(0); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 0, 0, 0, 0); + vec3_set(vtx.position, -p_x + _params.shear.x, -p_y - _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + { + auto vtx = _vertex_buffer->at(1); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 1, 0, 0, 0); + vec3_set(vtx.position, p_x + _params.shear.x, -p_y + _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + { + auto vtx = _vertex_buffer->at(2); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 0, 1, 0, 0); + vec3_set(vtx.position, -p_x - _params.shear.x, p_y - _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + { + auto vtx = _vertex_buffer->at(3); + *vtx.color = 0xFFFFFFFF; + vec4_set(vtx.uv[0], 1, 1, 0, 0); + vec3_set(vtx.position, p_x - _params.shear.x, p_y + _params.shear.y, 0); + vec3_transform(vtx.position, vtx.position, &ident); + } + } else if (_camera_mode == transform_mode::CORNER_PIN) { + // Corner Pin is rendered in Fragment. } _vertex_buffer->update(true); @@ -349,7 +387,8 @@ void transform_instance::video_render(gs_effect_t* effect) if (!effect) effect = default_effect; - if (!base_width || !base_height || !parent || !target || !_standard_effect) { // Skip if something is wrong. + if (!base_width || !base_height || !parent || !target || !_standard_effect + || !_transform_effect) { // Skip if something is wrong. obs_source_skip_video_filter(_self); return; } @@ -477,21 +516,55 @@ void transform_instance::video_render(gs_effect_t* effect) gs_matrix_scale3f(1.0, 1.0, 1.0); gs_matrix_translate3f(0., 0., -1.0); break; + case transform_mode::CORNER_PIN: + gs_ortho(0., 1., 0., 1., -farZ, farZ); + break; } - gs_load_vertexbuffer(_vertex_buffer->update(false)); - gs_load_indexbuffer(nullptr); - if (auto v = _standard_effect.get_parameter("InputA"); - v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Texture) { - v.set_texture(_mipmap_enabled - ? (_mipmap_texture ? _mipmap_texture->get_object() : _cache_texture->get_object()) - : _cache_texture->get_object()); - v.set_sampler(_sampler.get_object()); + if (_camera_mode != transform_mode::CORNER_PIN) { + gs_load_vertexbuffer(_vertex_buffer->update(false)); + gs_load_indexbuffer(nullptr); + if (auto v = _standard_effect.get_parameter("InputA"); + v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Texture) { + v.set_texture(_mipmap_enabled + ? (_mipmap_texture ? _mipmap_texture->get_object() : _cache_texture->get_object()) + : _cache_texture->get_object()); + v.set_sampler(_sampler.get_object()); + } + while (gs_effect_loop(_standard_effect.get_object(), "Draw")) { + gs_draw(GS_TRISTRIP, 0, _vertex_buffer->size()); + } + gs_load_vertexbuffer(nullptr); + } else { + gs_load_vertexbuffer(nullptr); + gs_load_indexbuffer(nullptr); + if (auto v = _transform_effect.get_parameter("InputA"); + v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Texture) { + v.set_texture(_mipmap_enabled + ? (_mipmap_texture ? _mipmap_texture->get_object() : _cache_texture->get_object()) + : _cache_texture->get_object()); + v.set_sampler(_sampler.get_object()); + } + if (auto v = _transform_effect.get_parameter("CornerTL"); + v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.tl); + } + if (auto v = _transform_effect.get_parameter("CornerTR"); + v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.tr); + } + if (auto v = _transform_effect.get_parameter("CornerBL"); + v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.bl); + } + if (auto v = _transform_effect.get_parameter("CornerBR"); + v.get_type() == ::streamfx::obs::gs::effect_parameter::type::Float2) { + v.set_float2(_corners.br); + } + while (gs_effect_loop(_transform_effect.get_object(), "CornerPin")) { + ::streamfx::gs_draw_fullscreen_tri(); + } } - while (gs_effect_loop(_standard_effect.get_object(), "Draw")) { - gs_draw(GS_TRISTRIP, 0, _vertex_buffer->size()); - } - gs_load_vertexbuffer(nullptr); gs_blend_state_pop(); } @@ -533,7 +606,7 @@ const char* transform_factory::get_name() void transform_factory::get_defaults2(obs_data_t* settings) { - obs_data_set_default_int(settings, ST_KEY_CAMERA_MODE, static_cast(transform_mode::ORTHOGRAPHIC)); + obs_data_set_default_int(settings, ST_KEY_CAMERA_MODE, static_cast(transform_mode::CORNER_PIN)); obs_data_set_default_double(settings, ST_KEY_CAMERA_FIELDOFVIEW, 90.0); obs_data_set_default_double(settings, ST_KEY_POSITION_X, 0); obs_data_set_default_double(settings, ST_KEY_POSITION_Y, 0); @@ -546,13 +619,21 @@ void transform_factory::get_defaults2(obs_data_t* settings) obs_data_set_default_double(settings, ST_KEY_SCALE_Y, 100); obs_data_set_default_double(settings, ST_KEY_SHEAR_X, 0); obs_data_set_default_double(settings, ST_KEY_SHEAR_Y, 0); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPLEFT "X", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPLEFT "Y", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPRIGHT "X", 100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_TOPRIGHT "Y", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMLEFT "X", -100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMLEFT "Y", 100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMRIGHT "X", 100.); + obs_data_set_default_double(settings, ST_KEY_CORNERS_BOTTOMRIGHT "Y", 100.); obs_data_set_default_bool(settings, ST_KEY_MIPMAPPING, false); } static bool modified_camera_mode(obs_properties_t* pr, obs_property_t*, obs_data_t* d) noexcept try { auto mode = static_cast(obs_data_get_int(d, ST_KEY_CAMERA_MODE)); - bool is_camera = true; + bool is_camera = mode != transform_mode::CORNER_PIN; bool is_perspective = (mode == transform_mode::PERSPECTIVE) && is_camera; bool is_orthographic = (mode == transform_mode::ORTHOGRAPHIC) && is_camera; @@ -563,6 +644,7 @@ try { obs_property_set_visible(obs_properties_get(pr, ST_I18N_SCALE), is_camera); obs_property_set_visible(obs_properties_get(pr, ST_I18N_SHEAR), is_camera); obs_property_set_visible(obs_properties_get(pr, ST_KEY_ROTATION_ORDER), is_camera); + obs_property_set_visible(obs_properties_get(pr, ST_I18N_CORNERS), !is_camera); return true; } catch (const std::exception& ex) { @@ -591,6 +673,8 @@ obs_properties_t* transform_factory::get_properties2(transform_instance* data) { // Projection Mode auto p = obs_properties_add_list(grp, ST_KEY_CAMERA_MODE, D_TRANSLATE(ST_I18N_CAMERA_MODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_CORNER_PIN), + static_cast(transform_mode::CORNER_PIN)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_ORTHOGRAPHIC), static_cast(transform_mode::ORTHOGRAPHIC)); obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_PERSPECTIVE), @@ -671,6 +755,77 @@ obs_properties_t* transform_factory::get_properties2(transform_instance* data) } } + { // Corners + + auto grp = obs_properties_create(); + { // Top Left + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_TOPLEFT "X", "X"}, + {ST_KEY_CORNERS_TOPLEFT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = + obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_TOPLEFT, D_TRANSLATE(ST_I18N_CORNERS_TOPLEFT), + OBS_GROUP_NORMAL, grp2); + } + { // Top Right + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_TOPRIGHT "X", "X"}, + {ST_KEY_CORNERS_TOPRIGHT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = + obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_TOPRIGHT, D_TRANSLATE(ST_I18N_CORNERS_TOPRIGHT), + OBS_GROUP_NORMAL, grp2); + } + { // Bottom Left + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_BOTTOMLEFT "X", "X"}, + {ST_KEY_CORNERS_BOTTOMLEFT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = + obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_BOTTOMLEFT, D_TRANSLATE(ST_I18N_CORNERS_BOTTOMLEFT), + OBS_GROUP_NORMAL, grp2); + } + { // Bottom Right + auto grp2 = obs_properties_create(); + + std::pair opts[] = { + {ST_KEY_CORNERS_BOTTOMRIGHT "X", "X"}, + {ST_KEY_CORNERS_BOTTOMRIGHT "Y", "Y"}, + }; + for (auto& opt : opts) { + auto p = + obs_properties_add_float_slider(grp2, opt.first.c_str(), opt.second.c_str(), -200.0, 200.0, 0.01); + obs_property_float_set_suffix(p, "%"); + } + + obs_properties_add_group(grp, ST_I18N_CORNERS_BOTTOMRIGHT, D_TRANSLATE(ST_I18N_CORNERS_BOTTOMRIGHT), + OBS_GROUP_NORMAL, grp2); + } + + obs_properties_add_group(pr, ST_I18N_CORNERS, D_TRANSLATE(ST_I18N_CORNERS), OBS_GROUP_NORMAL, grp); + } + { auto grp = obs_properties_create(); obs_properties_add_group(pr, S_ADVANCED, D_TRANSLATE(S_ADVANCED), OBS_GROUP_NORMAL, grp); diff --git a/source/filters/filter-transform.hpp b/source/filters/filter-transform.hpp index c9ed96ad..ec79898a 100644 --- a/source/filters/filter-transform.hpp +++ b/source/filters/filter-transform.hpp @@ -30,6 +30,7 @@ namespace streamfx::filter::transform { enum class transform_mode { ORTHOGRAPHIC = 0, PERSPECTIVE = 1, + CORNER_PIN = 2, }; class transform_instance : public obs::source_instance { @@ -43,9 +44,16 @@ namespace streamfx::filter::transform { vec3 scale; vec3 shear; } _params; + struct { + vec2 tl; + vec2 tr; + vec2 bl; + vec2 br; + } _corners; // Data streamfx::obs::gs::effect _standard_effect; + streamfx::obs::gs::effect _transform_effect; streamfx::obs::gs::sampler _sampler; // Cache