diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp index 6b2b83d2ab..6468d8049d 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include #include #include @@ -118,6 +120,10 @@ double MappingIndicator::GetScale() const namespace { +constexpr float SPHERE_SIZE = 0.7f; +constexpr float SPHERE_INDICATOR_DIST = 0.85f; +constexpr int SPHERE_POINT_COUNT = 200; + // Constructs a polygon by querying a radius at varying angles: template QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale, @@ -184,6 +190,22 @@ bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInp return current_radius > input_radius * ALLOWED_ERROR; } +template +void GenerateFibonacciSphere(int point_count, F&& callback) +{ + const float golden_angle = MathUtil::PI * (3.f - std::sqrt(5.f)); + + for (int i = 0; i != point_count; ++i) + { + const float z = (1.f / point_count - 1.f) + (2.f / point_count) * i; + const float r = std::sqrt(1.f - z * z); + const float x = std::cos(golden_angle * i) * r; + const float y = std::sin(golden_angle * i) * r; + + callback(Common::Vec3{x, y, z}); + } +} + } // namespace void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor) @@ -681,6 +703,199 @@ void ShakeMappingIndicator::DrawShake() } } +AccelerometerMappingIndicator::AccelerometerMappingIndicator(ControllerEmu::IMUAccelerometer* group) + : MappingIndicator(group), m_accel_group(*group) +{ +} + +void AccelerometerMappingIndicator::paintEvent(QPaintEvent*) +{ + const auto accel_state = m_accel_group.GetState(); + const auto state = accel_state.value_or(Common::Vec3{}); + + // Bounding box size: + const double scale = GetScale(); + + QPainter p(this); + p.translate(width() / 2, height() / 2); + + // Bounding box. + p.setBrush(GetBBoxBrush()); + p.setPen(GetBBoxPen()); + p.drawRect(-scale - 1, -scale - 1, scale * 2 + 1, scale * 2 + 1); + + // UI y-axis is opposite that of acceleration Z. + p.scale(1.0, -1.0); + + // Enable AA after drawing bounding box. + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + + const auto angle = std::acos(state.Normalized().Dot({0, 0, 1})); + const auto axis = state.Normalized().Cross({0, 0, 1}).Normalized(); + + // Odd checks to handle case of 0g (draw no sphere) and perfect up/down orientation. + const auto rotation = (!state.LengthSquared() || axis.LengthSquared() < 2) ? + Common::Matrix33::Rotate(angle, axis) : + Common::Matrix33::Identity(); + + // Draw sphere. + p.setPen(Qt::NoPen); + p.setBrush(GetRawInputColor()); + + GenerateFibonacciSphere(SPHERE_POINT_COUNT, [&](const Common::Vec3& point) { + const auto pt = rotation * point; + + if (pt.y > 0) + p.drawEllipse(QPointF(pt.x, pt.z) * scale * SPHERE_SIZE, 0.5f, 0.5f); + }); + + // Sphere outline. + p.setPen(GetRawInputColor()); + p.setBrush(Qt::NoBrush); + p.drawEllipse(QPointF{}, scale * SPHERE_SIZE, scale * SPHERE_SIZE); + + // Red dot upright target. + p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS / 2)); + p.drawEllipse(QPointF{0, SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + + // Red dot. + const auto point = rotation * Common::Vec3{0, 0, SPHERE_INDICATOR_DIST}; + if (point.y > 0 || Common::Vec2(point.x, point.z).Length() > SPHERE_SIZE) + { + p.setPen(Qt::NoPen); + p.setBrush(GetAdjustedInputColor()); + p.drawEllipse(QPointF(point.x, point.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + } + + // Blue dot target. + p.setPen(QPen(Qt::blue, INPUT_DOT_RADIUS / 2)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(QPointF{0, -SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + + // Blue dot. + const auto point2 = -point; + if (point2.y > 0 || Common::Vec2(point2.x, point2.z).Length() > SPHERE_SIZE) + { + p.setPen(Qt::NoPen); + p.setBrush(Qt::blue); + p.drawEllipse(QPointF(point2.x, point2.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + } + + // Only draw g-force text if acceleration data is present. + if (!accel_state.has_value()) + return; + + // G-force text: + p.setPen(GetTextColor()); + p.scale(1.0, -1.0); + p.drawText(QRectF(-2, 0, scale, scale), Qt::AlignBottom | Qt::AlignRight, + QString::fromStdString( + // i18n: "g" is the symbol for "gravitational force equivalent" (g-force). + fmt::format("{:.2f} g", state.Length() / WiimoteEmu::GRAVITY_ACCELERATION))); +} + +GyroMappingIndicator::GyroMappingIndicator(ControllerEmu::IMUGyroscope* group) + : MappingIndicator(group), m_gyro_group(*group), m_state(Common::Matrix33::Identity()) +{ +} + +void GyroMappingIndicator::paintEvent(QPaintEvent*) +{ + const auto gyro_state = m_gyro_group.GetState(); + const auto angular_velocity = gyro_state.value_or(Common::Vec3{}); + + m_state *= Common::Matrix33::RotateX(angular_velocity.x / -INDICATOR_UPDATE_FREQ) * + Common::Matrix33::RotateY(angular_velocity.y / INDICATOR_UPDATE_FREQ) * + Common::Matrix33::RotateZ(angular_velocity.z / -INDICATOR_UPDATE_FREQ); + + // Reset orientation when stable for a bit: + constexpr u32 STABLE_RESET_STEPS = INDICATOR_UPDATE_FREQ; + // This works well with my DS4 but a potentially noisy device might not behave. + const bool is_stable = angular_velocity.Length() < MathUtil::TAU / 30; + + if (!is_stable) + m_stable_steps = 0; + else if (m_stable_steps != STABLE_RESET_STEPS) + ++m_stable_steps; + + if (STABLE_RESET_STEPS == m_stable_steps) + m_state = Common::Matrix33::Identity(); + + // Use an empty rotation matrix if gyroscope data is not present. + const auto rotation = (gyro_state.has_value() ? m_state : Common::Matrix33{}); + + // Bounding box size: + const double scale = GetScale(); + + QPainter p(this); + p.translate(width() / 2, height() / 2); + + // Bounding box. + p.setBrush(GetBBoxBrush()); + p.setPen(GetBBoxPen()); + p.drawRect(-scale - 1, -scale - 1, scale * 2 + 1, scale * 2 + 1); + + // Enable AA after drawing bounding box. + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + + p.setPen(Qt::NoPen); + p.setBrush(GetRawInputColor()); + + GenerateFibonacciSphere(SPHERE_POINT_COUNT, [&, this](const Common::Vec3& point) { + const auto pt = rotation * point; + + if (pt.y > 0) + p.drawEllipse(QPointF(pt.x, pt.z) * scale * SPHERE_SIZE, 0.5f, 0.5f); + }); + + // Sphere outline. + p.setPen(GetRawInputColor()); + p.setBrush(Qt::NoBrush); + p.drawEllipse(QPointF{}, scale * SPHERE_SIZE, scale * SPHERE_SIZE); + + // Red dot upright target. + p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS / 2)); + p.drawEllipse(QPointF{0, -SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + + // Red dot. + const auto point = rotation * Common::Vec3{0, 0, -SPHERE_INDICATOR_DIST}; + if (point.y > 0 || Common::Vec2(point.x, point.z).Length() > SPHERE_SIZE) + { + p.setPen(Qt::NoPen); + p.setBrush(GetAdjustedInputColor()); + p.drawEllipse(QPointF(point.x, point.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + } + + // Blue dot target. + p.setPen(QPen(Qt::blue, INPUT_DOT_RADIUS / 2)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(QPointF{}, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + + // Blue dot. + const auto point2 = rotation * Common::Vec3{0, SPHERE_INDICATOR_DIST, 0}; + if (point2.y > 0 || Common::Vec2(point2.x, point2.z).Length() > SPHERE_SIZE) + { + p.setPen(Qt::NoPen); + p.setBrush(Qt::blue); + p.drawEllipse(QPointF(point2.x, point2.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + } + + // Only draw text if data is present. + if (!gyro_state.has_value()) + return; + + // Angle of red dot from starting position. + const auto angle = std::acos(point.Normalized().Dot({0, 0, -1})); + + // Angle text: + p.setPen(GetTextColor()); + p.drawText(QRectF(-2, 0, scale, scale), Qt::AlignBottom | Qt::AlignRight, + // i18n: "°" is the symbol for degrees (angular measurement). + QString::fromStdString(fmt::format("{:.2f} °", angle / MathUtil::TAU * 360))); +} + void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point) { // Bounding box size: diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h index 669f4f8d9e..c9f64d8be6 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h @@ -82,6 +82,28 @@ private: ControllerEmu::Shake& m_shake_group; }; +class AccelerometerMappingIndicator : public MappingIndicator +{ +public: + explicit AccelerometerMappingIndicator(ControllerEmu::IMUAccelerometer* group); + void paintEvent(QPaintEvent*) override; + +private: + ControllerEmu::IMUAccelerometer& m_accel_group; +}; + +class GyroMappingIndicator : public MappingIndicator +{ +public: + explicit GyroMappingIndicator(ControllerEmu::IMUGyroscope* group); + void paintEvent(QPaintEvent*) override; + +private: + ControllerEmu::IMUGyroscope& m_gyro_group; + Common::Matrix33 m_state; + u32 m_stable_steps = 0; +}; + class CalibrationWidget : public QToolButton { public: diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp index 6781bcb3da..9f5b2df5ee 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp @@ -67,6 +67,8 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con group->type == ControllerEmu::GroupType::Tilt || group->type == ControllerEmu::GroupType::MixedTriggers || group->type == ControllerEmu::GroupType::Force || + group->type == ControllerEmu::GroupType::IMUAccelerometer || + group->type == ControllerEmu::GroupType::IMUGyroscope || group->type == ControllerEmu::GroupType::Shake; const bool need_calibration = group->type == ControllerEmu::GroupType::Cursor || @@ -84,6 +86,15 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con indicator = new ShakeMappingIndicator(static_cast(group)); break; + case ControllerEmu::GroupType::IMUAccelerometer: + indicator = + new AccelerometerMappingIndicator(static_cast(group)); + break; + + case ControllerEmu::GroupType::IMUGyroscope: + indicator = new GyroMappingIndicator(static_cast(group)); + break; + default: indicator = new MappingIndicator(group); break;