summaryrefslogtreecommitdiff
path: root/Source/Core/InputCommon/ControllerEmu/StickGate.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Core/InputCommon/ControllerEmu/StickGate.cpp')
-rw-r--r--Source/Core/InputCommon/ControllerEmu/StickGate.cpp239
1 files changed, 193 insertions, 46 deletions
diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp
index 64cf238e17..8b4d64fb62 100644
--- a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp
+++ b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp
@@ -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