filter/transform: Implement 'Corner Pin' mode

'Perspective' and 'Orthographic' work great if you know what the parameters were to generate the exact object position, but what if you don't know them? That is where 'Corner Pin' comes in! With it you can specify the exact location of every corner down to the micro-pixel, instead of fiddling with parameters.

Fixes #565
This commit is contained in:
Michael Fabian 'Xaymar' Dirks 2021-10-22 03:02:12 +02:00
parent e0c6e55259
commit 150b728419
4 changed files with 381 additions and 90 deletions

View file

@ -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);
};
};

View file

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

View file

@ -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<streamfx::obs::gs::rendertarget>(GS_RGBA, GS_ZS_NONE);
_source_rt = std::make_shared<streamfx::obs::gs::rendertarget>(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<float>(obs_data_get_double(settings, ST_KEY_SHEAR_Y) / 100.0);
_params.shear.z = 0.0f;
}
{ // Corners
std::pair<std::string, float&> 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<float>(obs_data_get_double(settings, opt.first.c_str()) / 100.0);
}
}
// Mip-mapping
_mipmap_enabled = obs_data_get_bool(settings, ST_KEY_MIPMAPPING);
@ -252,9 +287,9 @@ void transform_instance::video_tick(float)
height = 1;
}
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;
@ -329,6 +364,9 @@ void transform_instance::video_tick(float)
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);
_update_mesh = false;
@ -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,8 +516,12 @@ 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;
}
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");
@ -492,6 +535,36 @@ void transform_instance::video_render(gs_effect_t* effect)
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();
}
}
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<int64_t>(transform_mode::ORTHOGRAPHIC));
obs_data_set_default_int(settings, ST_KEY_CAMERA_MODE, static_cast<int64_t>(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<transform_mode>(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<int64_t>(transform_mode::CORNER_PIN));
obs_property_list_add_int(p, D_TRANSLATE(ST_I18N_CAMERA_MODE_ORTHOGRAPHIC),
static_cast<int64_t>(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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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);

View file

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