@@ -8,23 +8,67 @@
#include " Common/Common.h"
#include " Common/MathUtil.h"
#include " Common/Matrix.h"
#include " Common/StringUtil.h"
#include " InputCommon/ControllerEmu/Control/Control.h"
#include " InputCommon/ControllerEmu/Setting/NumericSetting.h"
namespace
{
constexpr auto CALIBRATION_CONFIG_NAME = " Calibration" ;
constexpr auto CALIBRATION_DEFAULT_VALUE = 1.0 ;
constexpr auto CALIBRATION_CONFIG_SCALE = 100 ;
// Calculate distance to intersection of a ray with a line defined by two points.
double GetRayLineIntersection (Common::DVec2 ray, Common::DVec2 point1, Common::DVec2 point2)
{
const auto diff = point2 - point1;
const auto dot = diff.Dot ({-ray.y , ray.x });
if (std::abs (dot) < 0.00001 )
{
// Handle situation where both points are on top of eachother.
// This could occur if the user configures a single calibration value
// or when updating calibration.
return point1.Length ();
}
return diff.Cross (-point1) / dot;
}
Common::DVec2 GetPointFromAngleAndLength (double angle, double length)
{
return Common::DVec2{std::cos (angle), std::sin (angle)} * length;
}
} // namespace
namespace ControllerEmu
{
constexpr int ReshapableInput::CALIBRATION_SAMPLE_COUNT;
std::optional<u32> StickGate::GetIdealCalibrationSampleCount () const
{
return {};
}
OctagonStickGate::OctagonStickGate (ControlState radius) : m_radius(radius)
{
}
ControlState OctagonStickGate::GetRadiusAtAngle (double ang ) const
ControlState OctagonStickGate::GetRadiusAtAngle (double angle ) const
{
constexpr int sides = 8 ;
constexpr double sum_int_angles = (sides - 2 ) * MathUtil::PI;
constexpr double half_int_angle = sum_int_angles / sides / 2 ;
ang = std::fmod (ang , MathUtil::TAU / sides);
angle = std::fmod (angle , MathUtil::TAU / sides);
// Solve ASA triangle using The Law of Sines:
return m_radius / std::sin (MathUtil::PI - ang - half_int_angle) * std::sin (half_int_angle);
return m_radius / std::sin (MathUtil::PI - angle - half_int_angle) * std::sin (half_int_angle);
}
std::optional<u32> OctagonStickGate::GetIdealCalibrationSampleCount () const
{
return 8 ;
}
RoundStickGate::RoundStickGate (ControlState radius) : m_radius(radius)
@@ -40,50 +84,171 @@ SquareStickGate::SquareStickGate(ControlState half_width) : m_half_width(half_wi
{
}
ControlState SquareStickGate::GetRadiusAtAngle (double ang ) const
ControlState SquareStickGate::GetRadiusAtAngle (double angle ) const
{
constexpr double section_ang = MathUtil::TAU / 4 ;
return m_half_width / std::cos (std::fmod (ang + section_ang / 2 , section_ang) - section_ang / 2 );
constexpr double section_angle = MathUtil::TAU / 4 ;
return m_half_width /
std::cos (std::fmod (angle + section_angle / 2 , section_angle) - section_angle / 2 );
}
std::optional<u32> SquareStickGate::GetIdealCalibrationSampleCount () const
{
// Because angle:0 points to the right we must use 8 samples for our square.
return 8 ;
}
ReshapableInput::ReshapableInput (std::string name, std::string ui_name, GroupType type)
: ControlGroup(std::move(name), std::move(ui_name), type)
{
numeric_settings.emplace_back (std::make_unique<NumericSetting>(_trans (" Dead Zone" ), 0 , 0 , 50 ));
}
ControlState ReshapableInput::GetDeadzoneRadiusAtAngle (double ang ) const
ControlState ReshapableInput::GetDeadzoneRadiusAtAngle (double angle ) const
{
return CalculateInputShapeRadiusAtAngle (ang) * numeric_settings[SETTING_DEADZONE]->GetValue ();
// FYI: deadzone is scaled by input radius which allows the shape to match.
return GetInputRadiusAtAngle (angle) * numeric_settings[SETTING_DEADZONE]->GetValue ();
}
ControlState ReshapableInput::GetInputRadiusAtAngle (double ang ) const
ControlState ReshapableInput::GetInputRadiusAtAngle (double angle ) const
{
const ControlState radius =
CalculateInputShapeRadiusAtAngle (ang) * numeric_settings[SETTING_INPUT_RADIUS]->GetValue ();
// Clamp within the -1 to +1 square as input radius may be greater than 1.0:
return std::min (radius, SquareStickGate (1 ).GetRadiusAtAngle (ang));
// Handle the "default" state.
if (m_calibration.empty ())
{
return GetDefaultInputRadiusAtAngle (angle);
}
return GetCalibrationDataRadiusAtAngle (m_calibration, angle);
}
void ReshapableInput::AddReshapingSettings (ControlState default_radius, ControlState default_shape ,
int max_deadzone )
ControlState ReshapableInput::GetCalibrationDataRadiusAtAngle ( const CalibrationData& data ,
double angle )
{
// Allow radius greater than 1.0 for definitions of rounded squares
// This is ideal for Xbox controllers (and probably others)
numeric_settings.emplace_back (
std::make_unique<NumericSetting>(_trans (" Input Radius" ), default_radius, 0 , 140 ));
numeric_settings.emplace_back (
std::make_unique<NumericSetting>(_trans (" Input Shape" ), default_shape, 0 , 50 ));
numeric_settings.emplace_back (std::make_unique<NumericSetting>(_trans (" Dead Zone" ), 0 , 0 , 50 ));
const auto sample_pos = angle / MathUtil::TAU * data.size ();
// Interpolate the radius between 2 calibration samples.
const u32 sample1_index = u32 (sample_pos) % data.size ();
const u32 sample2_index = (sample1_index + 1 ) % data.size ();
const double sample1_angle = sample1_index * MathUtil::TAU / data.size ();
const double sample2_angle = sample2_index * MathUtil::TAU / data.size ();
return GetRayLineIntersection (GetPointFromAngleAndLength (angle, 1.0 ),
GetPointFromAngleAndLength (sample1_angle, data[sample1_index]),
GetPointFromAngleAndLength (sample2_angle, data[sample2_index]));
}
ControlState ReshapableInput::GetDefaultInputRadiusAtAngle (double angle) const
{
// This will normally be the same as the gate radius.
// Unless a sub-class is doing weird things with the gate radius (e.g. Tilt)
return GetGateRadiusAtAngle (angle);
}
void ReshapableInput::SetCalibrationToDefault ()
{
m_calibration.clear ();
}
void ReshapableInput::SetCalibrationFromGate (const StickGate& gate)
{
m_calibration.resize (gate.GetIdealCalibrationSampleCount ().value_or (CALIBRATION_SAMPLE_COUNT));
u32 i = 0 ;
for (auto & val : m_calibration)
val = gate.GetRadiusAtAngle (MathUtil::TAU * i++ / m_calibration.size ());
}
void ReshapableInput::UpdateCalibrationData (CalibrationData& data, Common::DVec2 point)
{
const auto angle_scale = MathUtil::TAU / data.size ();
const u32 calibration_index =
std::lround ((std::atan2 (point.y , point.x ) + MathUtil::TAU) / angle_scale) % data.size ();
const double calibration_angle = calibration_index * angle_scale;
auto & calibration_sample = data[calibration_index];
// Update closest sample from provided x,y.
calibration_sample = std::max (calibration_sample, point.Length ());
// Here we update all other samples in our calibration vector to maintain
// a convex polygon containing our new calibration point.
// This is required to properly fill in angles that cannot be gotten.
// (e.g. Keyboard input only has 8 possible angles)
// Note: Loop assumes an even sample count, which should not be a problem.
for (auto sample_offset = u32 (data.size () / 2 - 1 ); sample_offset > 1 ; --sample_offset)
{
const auto update_at_offset = [&](u32 offset1, u32 offset2) {
const u32 sample1_index = (calibration_index + offset1) % data.size ();
const double sample1_angle = sample1_index * angle_scale;
auto & sample1 = data[sample1_index];
const u32 sample2_index = (calibration_index + offset2) % data.size ();
const double sample2_angle = sample2_index * angle_scale;
auto & sample2 = data[sample2_index];
const double intersection =
GetRayLineIntersection (GetPointFromAngleAndLength (sample2_angle, 1.0 ),
GetPointFromAngleAndLength (sample1_angle, sample1),
GetPointFromAngleAndLength (calibration_angle, calibration_sample));
sample2 = std::max (sample2, intersection);
};
update_at_offset (sample_offset, sample_offset - 1 );
update_at_offset (u32 (data.size () - sample_offset), u32 (data.size () - sample_offset + 1 ));
}
}
const ReshapableInput::CalibrationData& ReshapableInput::GetCalibrationData () const
{
return m_calibration;
}
void ReshapableInput::SetCalibrationData (CalibrationData data)
{
m_calibration = std::move (data);
}
void ReshapableInput::LoadConfig (IniFile::Section* section, const std::string& default_device,
const std::string& base_name)
{
ControlGroup::LoadConfig (section, default_device, base_name);
const std::string group (base_name + name + ' /' );
std::string load_str;
section->Get (group + CALIBRATION_CONFIG_NAME, &load_str, " " );
const auto load_data = SplitString (load_str, ' ' );
m_calibration.assign (load_data.size (), CALIBRATION_DEFAULT_VALUE);
auto it = load_data.begin ();
for (auto & sample : m_calibration)
{
if (TryParse (*(it++), &sample))
sample /= CALIBRATION_CONFIG_SCALE;
}
}
void ReshapableInput::SaveConfig (IniFile::Section* section, const std::string& default_device,
const std::string& base_name)
{
ControlGroup::SaveConfig (section, default_device, base_name);
const std::string group (base_name + name + ' /' );
std::vector<std::string> save_data (m_calibration.size ());
std::transform (
m_calibration.begin (), m_calibration.end (), save_data.begin (),
[](ControlState val) { return StringFromFormat (" %.2f" , val * CALIBRATION_CONFIG_SCALE); });
section->Set (group + CALIBRATION_CONFIG_NAME, JoinStrings (save_data, " " ), " " );
}
ReshapableInput::ReshapeData ReshapableInput::Reshape (ControlState x, ControlState y,
ControlState modifier)
{
// TODO: make the AtAngle functions work with negative angles:
const ControlState ang = std::atan2 (y, x) + MathUtil::TAU;
const ControlState angle = std::atan2 (y, x) + MathUtil::TAU;
const ControlState gate_max_dist = GetGateRadiusAtAngle (ang );
const ControlState input_max_dist = GetInputRadiusAtAngle (ang );
const ControlState gate_max_dist = GetGateRadiusAtAngle (angle );
const ControlState input_max_dist = GetInputRadiusAtAngle (angle );
// If input radius is zero we apply no scaling.
// This is useful when mapping native controllers without knowing intimate radius details.
@@ -103,33 +268,15 @@ ReshapableInput::ReshapeData ReshapableInput::Reshape(ControlState x, ControlSta
}
// Apply deadzone as a percentage of the user-defined radius/shape:
const ControlState deadzone = GetDeadzoneRadiusAtAngle (ang );
const ControlState deadzone = GetDeadzoneRadiusAtAngle (angle );
dist = std::max (0.0 , dist - deadzone) / (1.0 - deadzone);
// Scale to the gate shape/radius:
dist = dist *= gate_max_dist;
x = MathUtil::Clamp (std::cos (ang ) * dist, -1.0 , 1.0 );
y = MathUtil::Clamp (std::sin (ang ) * dist, -1.0 , 1.0 );
x = MathUtil::Clamp (std::cos (angle ) * dist, -1.0 , 1.0 );
y = MathUtil::Clamp (std::sin (angle ) * dist, -1.0 , 1.0 );
return {x, y};
}
ControlState ReshapableInput::CalculateInputShapeRadiusAtAngle (double ang) const
{
const auto shape = numeric_settings[SETTING_INPUT_SHAPE]->GetValue () * 4.0 ;
if (shape < 1.0 )
{
// Between 0 and 25 return a shape between octagon and circle
const auto amt = shape;
return OctagonStickGate (1 ).GetRadiusAtAngle (ang) * (1 - amt) + amt;
}
else
{
// Between 25 and 50 return a shape between circle and square
const auto amt = shape - 1.0 ;
return (1 - amt) + SquareStickGate (1 ).GetRadiusAtAngle (ang) * amt;
}
}
} // namespace ControllerEmu