Merge pull request #7806 from jordan-woyak/wiimote-emu-swing-improve

WiimoteEmu: Reimplement tilt/swing/camera/orientation data using matrix math.
This commit is contained in:
Tilka 2019-03-05 23:37:00 +00:00 committed by GitHub
commit a865cc0bf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 897 additions and 476 deletions

View File

@ -9,21 +9,29 @@
namespace
{
void MatrixMul(int n, const float* a, const float* b, float* result)
// Multiply a NxM matrix by a NxP matrix.
template <int N, int M, int P, typename T>
auto MatrixMultiply(const std::array<T, N * M>& a, const std::array<T, M * P>& b)
-> std::array<T, N * P>
{
for (int i = 0; i < n; ++i)
std::array<T, N * P> result;
for (int n = 0; n != N; ++n)
{
for (int j = 0; j < n; ++j)
for (int p = 0; p != P; ++p)
{
float temp = 0;
for (int k = 0; k < n; ++k)
T temp = {};
for (int m = 0; m != M; ++m)
{
temp += a[i * n + k] * b[k * n + j];
temp += a[n * M + m] * b[m * P + p];
}
result[i * n + j] = temp;
result[n * P + p] = temp;
}
}
return result;
}
} // namespace
namespace Common
@ -39,8 +47,8 @@ Matrix33 Matrix33::Identity()
Matrix33 Matrix33::RotateX(float rad)
{
const float s = sin(rad);
const float c = cos(rad);
const float s = std::sin(rad);
const float c = std::cos(rad);
Matrix33 mtx = {};
mtx.data[0] = 1;
mtx.data[4] = c;
@ -52,8 +60,8 @@ Matrix33 Matrix33::RotateX(float rad)
Matrix33 Matrix33::RotateY(float rad)
{
const float s = sin(rad);
const float c = cos(rad);
const float s = std::sin(rad);
const float c = std::cos(rad);
Matrix33 mtx = {};
mtx.data[0] = c;
mtx.data[2] = s;
@ -65,8 +73,8 @@ Matrix33 Matrix33::RotateY(float rad)
Matrix33 Matrix33::RotateZ(float rad)
{
const float s = sin(rad);
const float c = cos(rad);
const float s = std::sin(rad);
const float c = std::cos(rad);
Matrix33 mtx = {};
mtx.data[0] = c;
mtx.data[1] = -s;
@ -87,20 +95,12 @@ Matrix33 Matrix33::Scale(const Vec3& vec)
void Matrix33::Multiply(const Matrix33& a, const Matrix33& b, Matrix33* result)
{
MatrixMul(3, a.data.data(), b.data.data(), result->data.data());
result->data = MatrixMultiply<3, 3, 3>(a.data, b.data);
}
void Matrix33::Multiply(const Matrix33& a, const Vec3& vec, Vec3* result)
{
for (int i = 0; i < 3; ++i)
{
result->data[i] = 0;
for (int k = 0; k < 3; ++k)
{
result->data[i] += a.data[i * 3 + k] * vec.data[k];
}
}
result->data = MatrixMultiply<3, 3, 1>(a.data, vec.data);
}
Matrix44 Matrix44::Identity()
@ -157,8 +157,32 @@ Matrix44 Matrix44::Shear(const float a, const float b)
return mtx;
}
Matrix44 Matrix44::Perspective(float fov_y, float aspect_ratio, float z_near, float z_far)
{
Matrix44 mtx{};
const float tan_half_fov_y = std::tan(fov_y / 2);
mtx.data[0] = 1 / (aspect_ratio * tan_half_fov_y);
mtx.data[5] = 1 / tan_half_fov_y;
mtx.data[10] = -(z_far + z_near) / (z_far - z_near);
mtx.data[11] = -(2 * z_far * z_near) / (z_far - z_near);
mtx.data[14] = -1;
return mtx;
}
void Matrix44::Multiply(const Matrix44& a, const Matrix44& b, Matrix44* result)
{
MatrixMul(4, a.data.data(), b.data.data(), result->data.data());
result->data = MatrixMultiply<4, 4, 4>(a.data, b.data);
}
Vec3 Matrix44::Transform(const Vec3& v, float w) const
{
const auto result = MatrixMultiply<4, 4, 1>(data, {v.x, v.y, v.z, w});
return Vec3{result[0], result[1], result[2]};
}
void Matrix44::Multiply(const Matrix44& a, const Vec4& vec, Vec4* result)
{
result->data = MatrixMultiply<4, 4, 1>(a.data, vec.data);
}
} // namespace Common

View File

@ -6,18 +6,25 @@
#include <array>
#include <cmath>
#include <type_traits>
// Tiny matrix/vector library.
// Used for things like Free-Look in the gfx backend.
namespace Common
{
union Vec3
template <typename T>
union TVec3
{
Vec3() = default;
Vec3(float _x, float _y, float _z) : data{_x, _y, _z} {}
TVec3() = default;
TVec3(T _x, T _y, T _z) : data{_x, _y, _z} {}
Vec3& operator+=(const Vec3& rhs)
T Dot(const TVec3& other) const { return x * other.x + y * other.y + z * other.z; }
T LengthSquared() const { return Dot(*this); }
T Length() const { return std::sqrt(LengthSquared()); }
TVec3 Normalized() const { return *this / Length(); }
TVec3& operator+=(const TVec3& rhs)
{
x += rhs.x;
y += rhs.y;
@ -25,21 +32,135 @@ union Vec3
return *this;
}
std::array<float, 3> data = {};
TVec3& operator-=(const TVec3& rhs)
{
x -= rhs.x;
y -= rhs.y;
z -= rhs.z;
return *this;
}
TVec3& operator*=(const TVec3& rhs)
{
x *= rhs.x;
y *= rhs.y;
z *= rhs.z;
return *this;
}
TVec3& operator/=(const TVec3& rhs)
{
x /= rhs.x;
y /= rhs.y;
z /= rhs.z;
return *this;
}
TVec3 operator-() const { return {-x, -y, -z}; }
std::array<T, 3> data = {};
struct
{
float x;
float y;
float z;
T x;
T y;
T z;
};
};
inline Vec3 operator+(Vec3 lhs, const Vec3& rhs)
template <typename T>
TVec3<T> operator+(TVec3<T> lhs, const TVec3<T>& rhs)
{
return lhs += rhs;
}
template <typename T>
TVec3<T> operator-(TVec3<T> lhs, const TVec3<T>& rhs)
{
return lhs -= rhs;
}
template <typename T>
TVec3<T> operator*(TVec3<T> lhs, const TVec3<T>& rhs)
{
return lhs *= rhs;
}
template <typename T>
inline TVec3<T> operator/(TVec3<T> lhs, const TVec3<T>& rhs)
{
return lhs /= rhs;
}
template <typename T>
TVec3<T> operator*(TVec3<T> lhs, std::common_type_t<T> scalar)
{
return lhs *= TVec3<T>{scalar, scalar, scalar};
}
template <typename T>
TVec3<T> operator/(TVec3<T> lhs, std::common_type_t<T> scalar)
{
return lhs /= TVec3<T>{scalar, scalar, scalar};
}
using Vec3 = TVec3<float>;
using DVec3 = TVec3<double>;
template <typename T>
union TVec4
{
TVec4() = default;
TVec4(TVec3<T> _vec, T _w) : TVec4{_vec.x, _vec.y, _vec.z, _w} {}
TVec4(T _x, T _y, T _z, T _w) : data{_x, _y, _z, _w} {}
TVec4& operator*=(const TVec4& rhs)
{
x *= rhs.x;
y *= rhs.y;
z *= rhs.z;
w *= rhs.w;
return *this;
}
TVec4& operator/=(const TVec4& rhs)
{
x /= rhs.x;
y /= rhs.y;
z /= rhs.z;
w /= rhs.w;
return *this;
}
TVec4& operator*=(T scalar) { return *this *= TVec4{scalar, scalar, scalar, scalar}; }
TVec4& operator/=(T scalar) { return *this /= TVec4{scalar, scalar, scalar, scalar}; }
std::array<T, 4> data = {};
struct
{
T x;
T y;
T z;
T w;
};
};
template <typename T>
TVec4<T> operator*(TVec4<T> lhs, std::common_type_t<T> scalar)
{
return lhs *= scalar;
}
template <typename T>
TVec4<T> operator/(TVec4<T> lhs, std::common_type_t<T> scalar)
{
return lhs /= scalar;
}
using Vec4 = TVec4<float>;
using DVec4 = TVec4<double>;
template <typename T>
union TVec2
{
@ -73,6 +194,13 @@ union TVec2
return *this;
}
TVec2& operator/=(T scalar)
{
x /= scalar;
y /= scalar;
return *this;
}
TVec2 operator-() const { return {-x, -y}; }
std::array<T, 2> data = {};
@ -102,6 +230,12 @@ TVec2<T> operator*(TVec2<T> lhs, T scalar)
return lhs *= scalar;
}
template <typename T>
TVec2<T> operator/(TVec2<T> lhs, T scalar)
{
return lhs /= scalar;
}
using Vec2 = TVec2<float>;
using DVec2 = TVec2<double>;
@ -123,10 +257,11 @@ public:
Matrix33& operator*=(const Matrix33& rhs)
{
Multiply(Matrix33(*this), rhs, this);
Multiply(*this, rhs, this);
return *this;
}
// Note: Row-major storage order.
std::array<float, 9> data;
};
@ -135,11 +270,10 @@ inline Matrix33 operator*(Matrix33 lhs, const Matrix33& rhs)
return lhs *= rhs;
}
inline Vec3 operator*(const Matrix33& lhs, const Vec3& rhs)
inline Vec3 operator*(const Matrix33& lhs, Vec3 rhs)
{
Vec3 result;
Matrix33::Multiply(lhs, rhs, &result);
return result;
Matrix33::Multiply(lhs, rhs, &rhs);
return rhs;
}
class Matrix44
@ -151,15 +285,21 @@ public:
static Matrix44 Translate(const Vec3& vec);
static Matrix44 Shear(const float a, const float b = 0);
static Matrix44 Perspective(float fov_y, float aspect_ratio, float z_near, float z_far);
static void Multiply(const Matrix44& a, const Matrix44& b, Matrix44* result);
static void Multiply(const Matrix44& a, const Vec4& vec, Vec4* result);
// For when a vec4 isn't needed a multiplication function that takes a Vec3 and w:
Vec3 Transform(const Vec3& point, float w) const;
Matrix44& operator*=(const Matrix44& rhs)
{
Multiply(Matrix44(*this), rhs, this);
Multiply(*this, rhs, this);
return *this;
}
// Note: Row-major storage order.
std::array<float, 16> data;
};
@ -167,4 +307,11 @@ inline Matrix44 operator*(Matrix44 lhs, const Matrix44& rhs)
{
return lhs *= rhs;
}
inline Vec4 operator*(const Matrix44& lhs, Vec4 rhs)
{
Matrix44::Multiply(lhs, rhs, &rhs);
return rhs;
}
} // namespace Common

View File

@ -10,24 +10,12 @@ namespace Config
// WiimoteInput.Settings
const ConfigInfo<double> WIIMOTE_INPUT_SWING_INTENSITY_FAST{{System::WiiPad, "Swing", "Fast"}, 4.5};
const ConfigInfo<double> WIIMOTE_INPUT_SWING_INTENSITY_MEDIUM{{System::WiiPad, "Swing", "Medium"},
2.5};
const ConfigInfo<double> WIIMOTE_INPUT_SWING_INTENSITY_SLOW{{System::WiiPad, "Swing", "Slow"}, 1.5};
const ConfigInfo<double> WIIMOTE_INPUT_SHAKE_INTENSITY_HARD{{System::WiiPad, "Shake", "Hard"}, 5.0};
const ConfigInfo<double> WIIMOTE_INPUT_SHAKE_INTENSITY_MEDIUM{{System::WiiPad, "Shake", "Medium"},
3.0};
const ConfigInfo<double> WIIMOTE_INPUT_SHAKE_INTENSITY_SOFT{{System::WiiPad, "Shake", "Soft"}, 2.0};
// Dynamic settings
const ConfigInfo<int> WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_HELD_FAST{
{System::WiiPad, "Dynamic_Swing", "FramesHeldFast"}, 100};
const ConfigInfo<int> WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_HELD_SLOW{
{System::WiiPad, "Dynamic_Swing", "FramesHeldSlow"}, 30};
const ConfigInfo<int> WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_LENGTH{
{System::WiiPad, "Dynamic_Swing", "FrameCount"}, 30};
const ConfigInfo<int> WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_HARD{
{System::WiiPad, "Dynamic_Shake", "FramesHeldHard"}, 45};
const ConfigInfo<int> WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_SOFT{
@ -36,18 +24,10 @@ const ConfigInfo<int> WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_LENGTH{
{System::WiiPad, "Dynamic_Shake", "FrameCount"}, 30};
// NunchuckInput.Settings
const ConfigInfo<double> NUNCHUK_INPUT_SWING_INTENSITY_FAST{
{System::WiiPad, "Nunchuk_Swing", "Fast"}, 4.5};
const ConfigInfo<double> NUNCHUK_INPUT_SWING_INTENSITY_MEDIUM{
{System::WiiPad, "Nunchuk_Swing", "Medium"}, 2.5};
const ConfigInfo<double> NUNCHUK_INPUT_SWING_INTENSITY_SLOW{
{System::WiiPad, "Nunchuk_Swing", "Slow"}, 1.5};
const ConfigInfo<double> NUNCHUK_INPUT_SHAKE_INTENSITY_HARD{
{System::WiiPad, "Nunchuk_Shake", "Hard"}, 5.0};
const ConfigInfo<double> NUNCHUK_INPUT_SHAKE_INTENSITY_MEDIUM{
{System::WiiPad, "Nunchuk_Shake", "Medium"}, 3.0};
const ConfigInfo<double> NUNCHUK_INPUT_SHAKE_INTENSITY_SOFT{
{System::WiiPad, "Nunchuk_Shake", "Soft"}, 2.0};
}
} // namespace Config

View File

@ -11,24 +11,11 @@ namespace Config
// Configuration Information
// WiimoteInput.Settings
extern const ConfigInfo<double> WIIMOTE_INPUT_SWING_INTENSITY_FAST;
extern const ConfigInfo<double> WIIMOTE_INPUT_SWING_INTENSITY_MEDIUM;
extern const ConfigInfo<double> WIIMOTE_INPUT_SWING_INTENSITY_SLOW;
extern const ConfigInfo<double> WIIMOTE_INPUT_SHAKE_INTENSITY_HARD;
extern const ConfigInfo<double> WIIMOTE_INPUT_SHAKE_INTENSITY_MEDIUM;
extern const ConfigInfo<double> WIIMOTE_INPUT_SHAKE_INTENSITY_SOFT;
// Below settings are for dynamic input only (based on how long the user holds a button)
extern const ConfigInfo<int>
WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_HELD_FAST; // How long button held constitutes a fast swing
extern const ConfigInfo<int>
WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_HELD_SLOW; // How long button held constitutes a slow swing
extern const ConfigInfo<int>
WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_LENGTH; // How long to execute the swing
extern const ConfigInfo<int>
WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_HARD; // How long button held constitutes a hard shake
extern const ConfigInfo<int>
@ -37,11 +24,6 @@ extern const ConfigInfo<int>
WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_LENGTH; // How long to execute a shake
// NunchuckInput.Settings
extern const ConfigInfo<double> NUNCHUK_INPUT_SWING_INTENSITY_FAST;
extern const ConfigInfo<double> NUNCHUK_INPUT_SWING_INTENSITY_MEDIUM;
extern const ConfigInfo<double> NUNCHUK_INPUT_SWING_INTENSITY_SLOW;
extern const ConfigInfo<double> NUNCHUK_INPUT_SHAKE_INTENSITY_HARD;
extern const ConfigInfo<double> NUNCHUK_INPUT_SHAKE_INTENSITY_MEDIUM;
extern const ConfigInfo<double> NUNCHUK_INPUT_SHAKE_INTENSITY_SOFT;

View File

@ -4,10 +4,12 @@
#include "Core/HW/WiimoteEmu/Camera.h"
#include <algorithm>
#include <cmath>
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Common/MathUtil.h"
#include "Common/Matrix.h"
#include "Core/HW/WiimoteCommon/WiimoteReport.h"
@ -49,83 +51,76 @@ int CameraLogic::BusWrite(u8 slave_addr, u8 addr, int count, const u8* data_in)
return RawWrite(&reg_data, addr, count, data_in);
}
void CameraLogic::Update(const ControllerEmu::Cursor::StateData& cursor,
const NormalizedAccelData& accel, bool sensor_bar_on_top)
void CameraLogic::Update(const Common::Matrix44& transform, bool sensor_bar_on_top)
{
double nsin;
using Common::Matrix33;
using Common::Matrix44;
using Common::Vec3;
using Common::Vec4;
// Ugly code to figure out the wiimote's current angle.
// TODO: Kill this.
double ax = accel.x;
double az = accel.z;
const double len = std::sqrt(ax * ax + az * az);
constexpr int CAMERA_WIDTH = 1024;
constexpr int CAMERA_HEIGHT = 768;
if (len)
// Wiibrew claims the camera FOV is about 33 deg by 23 deg.
// Unconfirmed but it seems to work well enough.
constexpr int CAMERA_FOV_X_DEG = 33;
constexpr int CAMERA_FOV_Y_DEG = 23;
constexpr auto CAMERA_FOV_Y = float(CAMERA_FOV_Y_DEG * MathUtil::TAU / 360);
constexpr auto CAMERA_ASPECT_RATIO = float(CAMERA_FOV_X_DEG) / CAMERA_FOV_Y_DEG;
// FYI: A real wiimote normally only returns 1 point for each LED cluster (2 total).
// Sending all 4 points can actually cause some stuttering issues.
constexpr int NUM_POINTS = 2;
// Range from 0-15. Small values (2-4) seem to be very typical.
// This is reduced based on distance from sensor bar.
constexpr int MAX_POINT_SIZE = 15;
// Sensor bar:
// Values are optimized for default settings in "Super Mario Galaxy 2"
// This seems to be acceptable for a good number of games.
constexpr float SENSOR_BAR_LED_SEPARATION = 0.2f;
const float sensor_bar_height = sensor_bar_on_top ? 0.11 : -0.11;
const std::array<Vec3, NUM_POINTS> leds{
Vec3{-SENSOR_BAR_LED_SEPARATION / 2, 0, sensor_bar_height},
Vec3{SENSOR_BAR_LED_SEPARATION / 2, 0, sensor_bar_height},
};
const auto camera_view = Matrix44::Perspective(CAMERA_FOV_Y, CAMERA_ASPECT_RATIO, 0.001f, 1000) *
Matrix44::FromMatrix33(Matrix33::RotateX(float(MathUtil::TAU / 4))) *
transform;
struct CameraPoint
{
ax /= len;
az /= len; // normalizing the vector
nsin = ax;
}
else
{
nsin = 0;
}
u16 x;
u16 y;
u8 size;
};
static constexpr int camWidth = 1024;
static constexpr int camHeight = 768;
static constexpr double bndleft = 0.78820266;
static constexpr double bndright = -0.78820266;
static constexpr double dist1 = 100.0 / camWidth; // this seems the optimal distance for zelda
static constexpr double dist2 = 1.2 * dist1;
// 0xFFFFs are interpreted as "not visible".
constexpr CameraPoint INVISIBLE_POINT{0xffff, 0xffff, 0xff};
constexpr int NUM_POINTS = 4;
std::array<CameraPoint, leds.size()> camera_points;
std::array<Common::Vec3, NUM_POINTS> v;
std::transform(leds.begin(), leds.end(), camera_points.begin(), [&](auto& v) {
const auto point = camera_view * Vec4(v, 1.0);
for (auto& vtx : v)
{
vtx.x = cursor.x * (bndright - bndleft) / 2 + (bndleft + bndright) / 2;
static constexpr double bndup = -0.315447;
static constexpr double bnddown = 0.85;
if (sensor_bar_on_top)
vtx.y = cursor.y * (bndup - bnddown) / 2 + (bndup + bnddown) / 2;
else
vtx.y = cursor.y * (bndup - bnddown) / 2 - (bndup + bnddown) / 2;
vtx.z = 0;
}
v[0].x -= (cursor.z * 0.5 + 1) * dist1;
v[1].x += (cursor.z * 0.5 + 1) * dist1;
v[2].x -= (cursor.z * 0.5 + 1) * dist2;
v[3].x += (cursor.z * 0.5 + 1) * dist2;
const auto scale = Common::Matrix33::Scale({1, camWidth / camHeight, 1});
const auto rot = Common::Matrix33::RotateZ(std::asin(nsin));
const auto tot = scale * rot;
u16 x[NUM_POINTS], y[NUM_POINTS];
memset(x, 0xFF, sizeof(x));
memset(y, 0xFF, sizeof(y));
for (std::size_t i = 0; i < v.size(); i++)
{
v[i] = tot * v[i];
if ((v[i].x < -1) || (v[i].x > 1) || (v[i].y < -1) || (v[i].y > 1))
continue;
x[i] = static_cast<u16>(lround((v[i].x + 1) / 2 * (camWidth - 1)));
y[i] = static_cast<u16>(lround((v[i].y + 1) / 2 * (camHeight - 1)));
if (x[i] >= camWidth || y[i] >= camHeight)
if (point.z > 0)
{
x[i] = -1;
y[i] = -1;
// FYI: Casting down vs. rounding seems to produce more symmetrical output.
const auto x = s32((1 - point.x / point.w) * CAMERA_WIDTH / 2);
const auto y = s32((1 - point.y / point.w) * CAMERA_HEIGHT / 2);
const auto point_size = std::lround(MAX_POINT_SIZE / point.w / 2);
if (x >= 0 && y >= 0 && x < CAMERA_WIDTH && y < CAMERA_HEIGHT)
return CameraPoint{u16(x), u16(y), u8(point_size)};
}
}
return INVISIBLE_POINT;
});
// IR data is read from offset 0x37 on real hardware
auto& data = reg_data.camera_data;
@ -139,65 +134,83 @@ void CameraLogic::Update(const ControllerEmu::Cursor::StateData& cursor,
switch (reg_data.mode)
{
case IR_MODE_BASIC:
for (unsigned int i = 0; i < 2; ++i)
for (std::size_t i = 0; i != camera_points.size() / 2; ++i)
{
IRBasic irdata = {};
irdata.x1 = static_cast<u8>(x[i * 2]);
irdata.x1hi = x[i * 2] >> 8;
irdata.y1 = static_cast<u8>(y[i * 2]);
irdata.y1hi = y[i * 2] >> 8;
const auto& p1 = camera_points[i * 2];
irdata.x1 = p1.x;
irdata.x1hi = p1.x >> 8;
irdata.y1 = p1.y;
irdata.y1hi = p1.y >> 8;
irdata.x2 = static_cast<u8>(x[i * 2 + 1]);
irdata.x2hi = x[i * 2 + 1] >> 8;
irdata.y2 = static_cast<u8>(y[i * 2 + 1]);
irdata.y2hi = y[i * 2 + 1] >> 8;
const auto& p2 = camera_points[i * 2 + 1];
irdata.x2 = p2.x;
irdata.x2hi = p2.x >> 8;
irdata.y2 = p2.y;
irdata.y2hi = p2.y >> 8;
Common::BitCastPtr<IRBasic>(data + i * sizeof(IRBasic)) = irdata;
}
break;
case IR_MODE_EXTENDED:
for (unsigned int i = 0; i < 4; ++i)
for (std::size_t i = 0; i != camera_points.size(); ++i)
{
if (x[i] < camWidth)
const auto& p = camera_points[i];
if (p.x < CAMERA_WIDTH)
{
IRExtended irdata = {};
irdata.x = static_cast<u8>(x[i]);
irdata.xhi = x[i] >> 8;
// TODO: Move this logic into IRExtended class?
irdata.x = p.x;
irdata.xhi = p.x >> 8;
irdata.y = static_cast<u8>(y[i]);
irdata.yhi = y[i] >> 8;
irdata.y = p.y;
irdata.yhi = p.y >> 8;
irdata.size = 10;
irdata.size = p.size;
Common::BitCastPtr<IRExtended>(data + i * sizeof(IRExtended)) = irdata;
}
}
break;
case IR_MODE_FULL:
for (unsigned int i = 0; i < 4; ++i)
for (std::size_t i = 0; i != camera_points.size(); ++i)
{
if (x[i] < camWidth)
const auto& p = camera_points[i];
if (p.x < CAMERA_WIDTH)
{
IRFull irdata = {};
irdata.x = static_cast<u8>(x[i]);
irdata.xhi = x[i] >> 8;
irdata.x = p.x;
irdata.xhi = p.x >> 8;
irdata.y = static_cast<u8>(y[i]);
irdata.yhi = y[i] >> 8;
irdata.y = p.y;
irdata.yhi = p.y >> 8;
irdata.size = 10;
irdata.size = p.size;
// TODO: implement these sensibly:
// TODO: do high bits of x/y min/max need to be set to zero?
irdata.xmin = 0;
irdata.ymin = 0;
irdata.xmax = 0;
irdata.ymax = 0;
// TODO: does size need to be scaled up?
// E.g. does size 15 cover the entire sensor range?
irdata.xmin = std::max(p.x - p.size, 0);
irdata.ymin = std::max(p.y - p.size, 0);
irdata.xmax = std::min(p.x + p.size, CAMERA_WIDTH);
irdata.ymax = std::min(p.y + p.size, CAMERA_HEIGHT);
// TODO: Is this maybe MSbs of the "intensity" value?
irdata.zero = 0;
irdata.intensity = 0;
constexpr int SUBPIXEL_RESOLUTION = 8;
constexpr long MAX_INTENSITY = 0xff;
// This is apparently the number of pixels the point takes up at 128x96 resolution.
// We simulate a circle that shrinks at sensor edges.
const auto intensity =
std::lround((irdata.xmax - irdata.xmin) * (irdata.ymax - irdata.ymin) /
SUBPIXEL_RESOLUTION / SUBPIXEL_RESOLUTION * MathUtil::TAU / 8);
irdata.intensity = u8(std::min(MAX_INTENSITY, intensity));
Common::BitCastPtr<IRFull>(data + i * sizeof(IRFull)) = irdata;
}

View File

@ -10,6 +10,11 @@
#include "Core/HW/WiimoteEmu/I2CBus.h"
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
namespace Common
{
class Matrix44;
}
namespace WiimoteEmu
{
// Four bytes for two objects. Filled with 0xFF if empty
@ -66,8 +71,7 @@ public:
void Reset();
void DoState(PointerWrap& p);
void Update(const ControllerEmu::Cursor::StateData& cursor, const NormalizedAccelData& accel,
bool sensor_bar_on_top);
void Update(const Common::Matrix44& transform, bool sensor_bar_on_top);
void SetEnabled(bool is_enabled);
static constexpr u8 I2C_ADDR = 0x58;

View File

@ -11,29 +11,71 @@
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "InputCommon/ControllerEmu/ControlGroup/Buttons.h"
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
#include "InputCommon/ControllerEmu/ControlGroup/Force.h"
#include "InputCommon/ControllerEmu/ControlGroup/Tilt.h"
namespace WiimoteEmu
namespace
{
constexpr int SHAKE_FREQ = 6;
// Frame count of one up/down shake
// < 9 no shake detection in "Wario Land: Shake It"
constexpr int SHAKE_STEP_MAX = ::Wiimote::UPDATE_FREQ / SHAKE_FREQ;
void EmulateShake(NormalizedAccelData* const accel, ControllerEmu::Buttons* const buttons_group,
const double intensity, u8* const shake_step)
// Given a velocity, acceleration, and maximum jerk value,
// calculate change in position after a stop in the shortest possible time.
// Used to smoothly adjust acceleration and come to complete stops at precise positions.
// Based on equations for motion with constant jerk.
// s = s0 + v0 t + a0 t^2 / 2 + j t^3 / 6
double CalculateStopDistance(double velocity, double acceleration, double max_jerk)
{
// Math below expects velocity to be non-negative.
const auto velocity_flip = (velocity < 0 ? -1 : 1);
const auto v_0 = velocity * velocity_flip;
const auto a_0 = acceleration * velocity_flip;
const auto j = max_jerk;
// Time to reach zero acceleration.
const auto t_0 = a_0 / j;
// Distance to reach zero acceleration.
const auto d_0 = std::pow(a_0, 3) / (3 * j * j) + (a_0 * v_0) / j;
// Velocity at zero acceleration.
const auto v_1 = v_0 + a_0 * std::abs(t_0) - std::copysign(j * t_0 * t_0 / 2, t_0);
// Distance to complete stop.
const auto d_1 = std::copysign(std::pow(std::abs(v_1), 3.0 / 2), v_1) / std::sqrt(j);
return (d_0 + d_1) * velocity_flip;
}
double CalculateStopDistance(double velocity, double max_accel)
{
return velocity * velocity / (2 * std::copysign(max_accel, velocity));
}
} // namespace
namespace WiimoteEmu
{
Common::Vec3 EmulateShake(ControllerEmu::Buttons* const buttons_group, const double intensity,
u8* const shake_step)
{
// shake is a bitfield of X,Y,Z shake button states
static const unsigned int btns[] = {0x01, 0x02, 0x04};
unsigned int shake = 0;
buttons_group->GetState(&shake, btns);
for (int i = 0; i != 3; ++i)
Common::Vec3 accel;
for (std::size_t i = 0; i != accel.data.size(); ++i)
{
if (shake & (1 << i))
{
(&(accel->x))[i] += std::sin(MathUtil::TAU * shake_step[i] / SHAKE_STEP_MAX) * intensity;
accel.data[i] = std::sin(MathUtil::TAU * shake_step[i] / SHAKE_STEP_MAX) * intensity *
GRAVITY_ACCELERATION;
shake_step[i] = (shake_step[i] + 1) % SHAKE_STEP_MAX;
}
else
@ -41,18 +83,22 @@ void EmulateShake(NormalizedAccelData* const accel, ControllerEmu::Buttons* cons
shake_step[i] = 0;
}
}
return accel;
}
void EmulateDynamicShake(NormalizedAccelData* const accel, DynamicData& dynamic_data,
ControllerEmu::Buttons* const buttons_group,
const DynamicConfiguration& config, u8* const shake_step)
Common::Vec3 EmulateDynamicShake(DynamicData& dynamic_data,
ControllerEmu::Buttons* const buttons_group,
const DynamicConfiguration& config, u8* const shake_step)
{
// shake is a bitfield of X,Y,Z shake button states
static const unsigned int btns[] = {0x01, 0x02, 0x04};
unsigned int shake = 0;
buttons_group->GetState(&shake, btns);
for (int i = 0; i != 3; ++i)
Common::Vec3 accel;
for (std::size_t i = 0; i != accel.data.size(); ++i)
{
if ((shake & (1 << i)) && dynamic_data.executing_frames_left[i] == 0)
{
@ -60,8 +106,8 @@ void EmulateDynamicShake(NormalizedAccelData* const accel, DynamicData& dynamic_
}
else if (dynamic_data.executing_frames_left[i] > 0)
{
(&(accel->x))[i] +=
std::sin(MathUtil::TAU * shake_step[i] / SHAKE_STEP_MAX) * dynamic_data.intensity[i];
accel.data[i] = std::sin(MathUtil::TAU * shake_step[i] / SHAKE_STEP_MAX) *
dynamic_data.intensity[i] * GRAVITY_ACCELERATION;
shake_step[i] = (shake_step[i] + 1) % SHAKE_STEP_MAX;
dynamic_data.executing_frames_left[i]--;
}
@ -87,178 +133,160 @@ void EmulateDynamicShake(NormalizedAccelData* const accel, DynamicData& dynamic_
shake_step[i] = 0;
}
}
return accel;
}
void EmulateTilt(NormalizedAccelData* const accel, ControllerEmu::Tilt* const tilt_group,
const bool sideways, const bool upright)
void EmulateTilt(RotationalState* state, ControllerEmu::Tilt* const tilt_group, float time_elapsed)
{
// 180 degrees
const ControllerEmu::Tilt::StateData state = tilt_group->GetState();
const ControlState roll = state.x * MathUtil::PI;
const ControlState pitch = state.y * MathUtil::PI;
const auto target = tilt_group->GetState();
// Some notes that no one will understand but me :p
// left, forward, up
// lr/ left == negative for all orientations
// ud/ up == negative for upright longways
// fb/ forward == positive for (sideways flat)
// 180 degrees is currently the max tilt value.
const ControlState roll = target.x * MathUtil::PI;
const ControlState pitch = target.y * MathUtil::PI;
// Determine which axis is which direction
const u32 ud = upright ? (sideways ? 0 : 1) : 2;
const u32 lr = sideways;
const u32 fb = upright ? 2 : (sideways ? 0 : 1);
// TODO: expose this setting in UI:
constexpr auto MAX_ACCEL = float(MathUtil::TAU * 50);
// Sign fix
std::array<int, 3> sgn{{-1, 1, 1}};
if (sideways && !upright)
sgn[fb] *= -1;
if (!sideways && upright)
sgn[ud] *= -1;
(&accel->x)[ud] = (sin((MathUtil::PI / 2) - std::max(fabs(roll), fabs(pitch)))) * sgn[ud];
(&accel->x)[lr] = -sin(roll) * sgn[lr];
(&accel->x)[fb] = sin(pitch) * sgn[fb];
ApproachAngleWithAccel(state, Common::Vec3(pitch, -roll, 0), MAX_ACCEL, time_elapsed);
}
void EmulateSwing(NormalizedAccelData* const accel, ControllerEmu::Force* const swing_group,
const double intensity, const bool sideways, const bool upright)
void EmulateSwing(MotionState* state, ControllerEmu::Force* swing_group, float time_elapsed)
{
const ControllerEmu::Force::StateData swing = swing_group->GetState();
const auto target = swing_group->GetState();
// Determine which axis is which direction
const std::array<int, 3> axis_map{{
upright ? (sideways ? 0 : 1) : 2, // up/down
sideways, // left/right
upright ? 2 : (sideways ? 0 : 1), // forward/backward
}};
// Note. Y/Z swapped because X/Y axis to the swing_group is X/Z to the wiimote.
// X is negated because Wiimote X+ is to the left.
ApproachPositionWithJerk(state, {-target.x, -target.z, target.y}, swing_group->GetMaxJerk(),
time_elapsed);
// Some orientations have up as positive, some as negative
// same with forward
std::array<s8, 3> g_dir{{-1, -1, -1}};
if (sideways && !upright)
g_dir[axis_map[2]] *= -1;
if (!sideways && upright)
g_dir[axis_map[0]] *= -1;
// Just jump to our target angle scaled by our progress to the target position.
// TODO: If we wanted to be less hacky we could use ApproachAngleWithAccel.
const auto angle = state->position / swing_group->GetMaxDistance() * swing_group->GetTwistAngle();
for (std::size_t i = 0; i < swing.size(); ++i)
(&accel->x)[axis_map[i]] += swing[i] * g_dir[i] * intensity;
const auto old_angle = state->angle;
state->angle = {-angle.z, 0, angle.x};
// Update velocity based on change in angle.
state->angular_velocity = state->angle - old_angle;
}
void EmulateDynamicSwing(NormalizedAccelData* const accel, DynamicData& dynamic_data,
ControllerEmu::Force* const swing_group,
const DynamicConfiguration& config, const bool sideways,
const bool upright)
WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g,
u16 one_g)
{
const ControllerEmu::Force::StateData swing = swing_group->GetState();
const auto scaled_accel = accel * (one_g - zero_g) / float(GRAVITY_ACCELERATION);
// Determine which axis is which direction
const std::array<int, 3> axis_map{{
upright ? (sideways ? 0 : 1) : 2, // up/down
sideways, // left/right
upright ? 2 : (sideways ? 0 : 1), // forward/backward
}};
// 10-bit integers.
constexpr long MAX_VALUE = (1 << 10) - 1;
// Some orientations have up as positive, some as negative
// same with forward
std::array<s8, 3> g_dir{{-1, -1, -1}};
if (sideways && !upright)
g_dir[axis_map[2]] *= -1;
if (!sideways && upright)
g_dir[axis_map[0]] *= -1;
return {u16(MathUtil::Clamp(std::lround(scaled_accel.x + zero_g), 0l, MAX_VALUE)),
u16(MathUtil::Clamp(std::lround(scaled_accel.y + zero_g), 0l, MAX_VALUE)),
u16(MathUtil::Clamp(std::lround(scaled_accel.z + zero_g), 0l, MAX_VALUE))};
}
for (std::size_t i = 0; i < swing.size(); ++i)
Common::Matrix44 EmulateCursorMovement(ControllerEmu::Cursor* ir_group)
{
const auto cursor = ir_group->GetState(true);
using Common::Matrix33;
using Common::Matrix44;
// Values are optimized for default settings in "Super Mario Galaxy 2"
// This seems to be acceptable for a good number of games.
constexpr float YAW_ANGLE = 0.1472f;
constexpr float PITCH_ANGLE = 0.121f;
// Nintendo recommends a distance of 1-3 meters.
constexpr float NEUTRAL_DISTANCE = 2.f;
constexpr float MOVE_DISTANCE = 1.f;
return Matrix44::Translate({0, MOVE_DISTANCE * float(cursor.z), 0}) *
Matrix44::FromMatrix33(Matrix33::RotateX(PITCH_ANGLE * cursor.y) *
Matrix33::RotateZ(YAW_ANGLE * cursor.x)) *
Matrix44::Translate({0, -NEUTRAL_DISTANCE, 0});
}
void ApproachAngleWithAccel(RotationalState* state, const Common::Vec3& angle_target,
float max_accel, float time_elapsed)
{
const auto stop_distance =
Common::Vec3(CalculateStopDistance(state->angular_velocity.x, max_accel),
CalculateStopDistance(state->angular_velocity.y, max_accel),
CalculateStopDistance(state->angular_velocity.z, max_accel));
const auto offset = angle_target - state->angle;
const auto stop_offset = offset - stop_distance;
const Common::Vec3 accel{std::copysign(max_accel, stop_offset.x),
std::copysign(max_accel, stop_offset.y),
std::copysign(max_accel, stop_offset.z)};
state->angular_velocity += accel * time_elapsed;
const auto change_in_angle =
state->angular_velocity * time_elapsed + accel * time_elapsed * time_elapsed / 2;
for (std::size_t i = 0; i != offset.data.size(); ++i)
{
if (swing[i] > 0 && dynamic_data.executing_frames_left[i] == 0)
// If new velocity will overshoot assume we would have stopped right on target.
// TODO: Improve check to see if less accel would have caused undershoot.
if ((change_in_angle.data[i] / offset.data[i]) > 1.0)
{
dynamic_data.timing[i]++;
state->angular_velocity.data[i] = 0;
state->angle.data[i] = angle_target.data[i];
}
else if (dynamic_data.executing_frames_left[i] > 0)
else
{
(&accel->x)[axis_map[i]] += g_dir[i] * dynamic_data.intensity[i];
dynamic_data.executing_frames_left[i]--;
}
else if (swing[i] == 0 && dynamic_data.timing[i] > 0)
{
if (dynamic_data.timing[i] > config.frames_needed_for_high_intensity)
{
dynamic_data.intensity[i] = config.high_intensity;
}
else if (dynamic_data.timing[i] < config.frames_needed_for_low_intensity)
{
dynamic_data.intensity[i] = config.low_intensity;
}
else
{
dynamic_data.intensity[i] = config.med_intensity;
}
dynamic_data.timing[i] = 0;
dynamic_data.executing_frames_left[i] = config.frames_to_execute;
state->angle.data[i] += change_in_angle.data[i];
}
}
}
WiimoteCommon::DataReportBuilder::AccelData DenormalizeAccelData(const NormalizedAccelData& accel,
u16 zero_g, u16 one_g)
void ApproachPositionWithJerk(PositionalState* state, const Common::Vec3& position_target,
float max_jerk, float time_elapsed)
{
const u8 accel_range = one_g - zero_g;
const auto stop_distance =
Common::Vec3(CalculateStopDistance(state->velocity.x, state->acceleration.x, max_jerk),
CalculateStopDistance(state->velocity.y, state->acceleration.y, max_jerk),
CalculateStopDistance(state->velocity.z, state->acceleration.z, max_jerk));
const s32 unclamped_x = (s32)(accel.x * accel_range + zero_g);
const s32 unclamped_y = (s32)(accel.y * accel_range + zero_g);
const s32 unclamped_z = (s32)(accel.z * accel_range + zero_g);
const auto offset = position_target - state->position;
const auto stop_offset = offset - stop_distance;
WiimoteCommon::DataReportBuilder::AccelData result;
const Common::Vec3 jerk{std::copysign(max_jerk, stop_offset.x),
std::copysign(max_jerk, stop_offset.y),
std::copysign(max_jerk, stop_offset.z)};
result.x = MathUtil::Clamp<u16>(unclamped_x, 0, 0x3ff);
result.y = MathUtil::Clamp<u16>(unclamped_y, 0, 0x3ff);
result.z = MathUtil::Clamp<u16>(unclamped_z, 0, 0x3ff);
state->acceleration += jerk * time_elapsed;
return result;
state->velocity += state->acceleration * time_elapsed + jerk * time_elapsed * time_elapsed / 2;
const auto change_in_position = state->velocity * time_elapsed +
state->acceleration * time_elapsed * time_elapsed / 2 +
jerk * time_elapsed * time_elapsed * time_elapsed / 6;
for (std::size_t i = 0; i != offset.data.size(); ++i)
{
// If new velocity will overshoot assume we would have stopped right on target.
// TODO: Improve check to see if less jerk would have caused undershoot.
if ((change_in_position.data[i] / offset.data[i]) > 1.0)
{
state->acceleration.data[i] = 0;
state->velocity.data[i] = 0;
state->position.data[i] = position_target.data[i];
}
else
{
state->position.data[i] += change_in_position.data[i];
}
}
}
void Wiimote::GetAccelData(NormalizedAccelData* accel)
Common::Matrix33 GetRotationalMatrix(const Common::Vec3& angle)
{
const bool is_sideways = IsSideways();
const bool is_upright = IsUpright();
EmulateTilt(accel, m_tilt, is_sideways, is_upright);
DynamicConfiguration swing_config;
swing_config.low_intensity = Config::Get(Config::WIIMOTE_INPUT_SWING_INTENSITY_SLOW);
swing_config.med_intensity = Config::Get(Config::WIIMOTE_INPUT_SWING_INTENSITY_MEDIUM);
swing_config.high_intensity = Config::Get(Config::WIIMOTE_INPUT_SWING_INTENSITY_FAST);
swing_config.frames_needed_for_high_intensity =
Config::Get(Config::WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_HELD_FAST);
swing_config.frames_needed_for_low_intensity =
Config::Get(Config::WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_HELD_SLOW);
swing_config.frames_to_execute = Config::Get(Config::WIIMOTE_INPUT_SWING_DYNAMIC_FRAMES_LENGTH);
EmulateSwing(accel, m_swing, Config::Get(Config::WIIMOTE_INPUT_SWING_INTENSITY_MEDIUM),
is_sideways, is_upright);
EmulateSwing(accel, m_swing_slow, Config::Get(Config::WIIMOTE_INPUT_SWING_INTENSITY_SLOW),
is_sideways, is_upright);
EmulateSwing(accel, m_swing_fast, Config::Get(Config::WIIMOTE_INPUT_SWING_INTENSITY_FAST),
is_sideways, is_upright);
EmulateDynamicSwing(accel, m_swing_dynamic_data, m_swing_dynamic, swing_config, is_sideways,
is_upright);
DynamicConfiguration shake_config;
shake_config.low_intensity = Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_SOFT);
shake_config.med_intensity = Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_MEDIUM);
shake_config.high_intensity = Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_HARD);
shake_config.frames_needed_for_high_intensity =
Config::Get(Config::WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_HARD);
shake_config.frames_needed_for_low_intensity =
Config::Get(Config::WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_SOFT);
shake_config.frames_to_execute = Config::Get(Config::WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_LENGTH);
EmulateShake(accel, m_shake, Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_MEDIUM),
m_shake_step.data());
EmulateShake(accel, m_shake_soft, Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_SOFT),
m_shake_soft_step.data());
EmulateShake(accel, m_shake_hard, Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_HARD),
m_shake_hard_step.data());
EmulateDynamicShake(accel, m_shake_dynamic_data, m_shake_dynamic, shake_config,
m_shake_dynamic_step.data());
return Common::Matrix33::RotateZ(angle.z) * Common::Matrix33::RotateY(angle.y) *
Common::Matrix33::RotateX(angle.x);
}
} // namespace WiimoteEmu

View File

@ -6,20 +6,19 @@
#include <array>
#include "Common/Matrix.h"
#include "Core/HW/WiimoteCommon/DataReport.h"
#include "InputCommon/ControllerEmu/ControlGroup/Buttons.h"
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
#include "InputCommon/ControllerEmu/ControlGroup/Force.h"
#include "InputCommon/ControllerEmu/ControlGroup/Tilt.h"
namespace WiimoteEmu
{
struct NormalizedAccelData
{
// Unit is 1G
double x, y, z;
};
constexpr double GRAVITY_ACCELERATION = 9.80665;
// Used for a dynamic swing or shake
// Used for dynamic shake
// TODO: kill this.
struct DynamicData
{
std::array<int, 3> timing; // Hold length in frames for each axis
@ -27,8 +26,36 @@ struct DynamicData
std::array<int, 3> executing_frames_left; // Number of frames to execute the intensity operation
};
// Used for a dynamic swing or shake.
struct PositionalState
{
Common::Vec3 position;
Common::Vec3 velocity;
Common::Vec3 acceleration;
};
struct RotationalState
{
Common::Vec3 angle;
Common::Vec3 angular_velocity;
};
// Contains both positional and rotational state.
struct MotionState : PositionalState, RotationalState
{
};
// Build a rotational matrix from euler angles.
Common::Matrix33 GetRotationalMatrix(const Common::Vec3& angle);
void ApproachPositionWithJerk(PositionalState* state, const Common::Vec3& target, float max_jerk,
float time_elapsed);
void ApproachAngleWithAccel(RotationalState* state, const Common::Vec3& target, float max_accel,
float time_elapsed);
// Used for dynamic shake.
// This is used to pass in data that defines the dynamic action
// TODO: kill this
struct DynamicConfiguration
{
double low_intensity;
@ -43,24 +70,18 @@ struct DynamicConfiguration
int frames_to_execute; // How many frames should we execute the action for?
};
void EmulateShake(NormalizedAccelData* accel, ControllerEmu::Buttons* buttons_group,
double intensity, u8* shake_step);
Common::Vec3 EmulateShake(ControllerEmu::Buttons* buttons_group, double intensity, u8* shake_step);
void EmulateDynamicShake(NormalizedAccelData* accel, DynamicData& dynamic_data,
ControllerEmu::Buttons* buttons_group, const DynamicConfiguration& config,
u8* shake_step);
Common::Vec3 EmulateDynamicShake(DynamicData& dynamic_data, ControllerEmu::Buttons* buttons_group,
const DynamicConfiguration& config, u8* shake_step);
void EmulateTilt(NormalizedAccelData* accel, ControllerEmu::Tilt* tilt_group, bool sideways = false,
bool upright = false);
void EmulateTilt(RotationalState* state, ControllerEmu::Tilt* tilt_group, float time_elapsed);
void EmulateSwing(MotionState* state, ControllerEmu::Force* swing_group, float time_elapsed);
void EmulateSwing(NormalizedAccelData* accel, ControllerEmu::Force* swing_group, double intensity,
bool sideways = false, bool upright = false);
// Convert m/s/s acceleration data to the format used by Wiimote/Nunchuk (10-bit unsigned integers).
WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g,
u16 one_g);
void EmulateDynamicSwing(NormalizedAccelData* accel, DynamicData& dynamic_data,
ControllerEmu::Force* swing_group, const DynamicConfiguration& config,
bool sideways = false, bool upright = false);
WiimoteCommon::DataReportBuilder::AccelData DenormalizeAccelData(const NormalizedAccelData& accel,
u16 zero_g, u16 one_g);
Common::Matrix44 EmulateCursorMovement(ControllerEmu::Cursor* ir_group);
} // namespace WiimoteEmu

View File

@ -583,6 +583,9 @@ void Wiimote::DoState(PointerWrap& p)
GetActiveExtension()->DoState(p);
// Dynamics
p.Do(m_swing_state);
p.Do(m_tilt_state);
// TODO: clean this up:
p.Do(m_shake_step);

View File

@ -98,6 +98,8 @@ protected:
Register m_reg = {};
void DoState(PointerWrap& p) override;
private:
static constexpr u8 ENCRYPTION_ENABLED = 0xaa;
@ -105,8 +107,6 @@ private:
int BusRead(u8 slave_addr, u8 addr, int count, u8* data_out) override;
int BusWrite(u8 slave_addr, u8 addr, int count, const u8* data_in) override;
void DoState(PointerWrap& p) override;
};
} // namespace WiimoteEmu

View File

@ -13,6 +13,7 @@
#include "Common/CommonTypes.h"
#include "Common/MathUtil.h"
#include "Core/Config/WiimoteInputSettings.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "InputCommon/ControllerEmu/Control/Input.h"
@ -45,8 +46,6 @@ Nunchuk::Nunchuk() : EncryptedExtension(_trans("Nunchuk"))
// swing
groups.emplace_back(m_swing = new ControllerEmu::Force(_trans("Swing")));
groups.emplace_back(m_swing_slow = new ControllerEmu::Force("SwingSlow"));
groups.emplace_back(m_swing_fast = new ControllerEmu::Force("SwingFast"));
// tilt
groups.emplace_back(m_tilt = new ControllerEmu::Tilt(_trans("Tilt")));
@ -96,32 +95,32 @@ void Nunchuk::Update()
++nc_data.jy;
}
NormalizedAccelData accel;
// tilt
EmulateTilt(&accel, m_tilt);
// swing
EmulateSwing(&accel, m_swing, Config::Get(Config::NUNCHUK_INPUT_SWING_INTENSITY_MEDIUM));
EmulateSwing(&accel, m_swing_slow, Config::Get(Config::NUNCHUK_INPUT_SWING_INTENSITY_SLOW));
EmulateSwing(&accel, m_swing_fast, Config::Get(Config::NUNCHUK_INPUT_SWING_INTENSITY_FAST));
// shake
EmulateShake(&accel, m_shake, Config::Get(Config::NUNCHUK_INPUT_SHAKE_INTENSITY_MEDIUM),
m_shake_step.data());
EmulateShake(&accel, m_shake_soft, Config::Get(Config::NUNCHUK_INPUT_SHAKE_INTENSITY_SOFT),
m_shake_soft_step.data());
EmulateShake(&accel, m_shake_hard, Config::Get(Config::NUNCHUK_INPUT_SHAKE_INTENSITY_HARD),
m_shake_hard_step.data());
// buttons
m_buttons->GetState(&nc_data.bt.hex, nunchuk_button_bitmasks.data());
// flip the button bits :/
nc_data.bt.hex ^= 0x03;
// Acceleration data:
EmulateSwing(&m_swing_state, m_swing, 1.f / ::Wiimote::UPDATE_FREQ);
EmulateTilt(&m_tilt_state, m_tilt, 1.f / ::Wiimote::UPDATE_FREQ);
const auto transformation =
GetRotationalMatrix(-m_tilt_state.angle) * GetRotationalMatrix(-m_swing_state.angle);
Common::Vec3 accel = transformation * (m_swing_state.acceleration +
Common::Vec3(0, 0, float(GRAVITY_ACCELERATION)));
// shake
accel += EmulateShake(m_shake, Config::Get(Config::NUNCHUK_INPUT_SHAKE_INTENSITY_MEDIUM),
m_shake_step.data());
accel += EmulateShake(m_shake_soft, Config::Get(Config::NUNCHUK_INPUT_SHAKE_INTENSITY_SOFT),
m_shake_soft_step.data());
accel += EmulateShake(m_shake_hard, Config::Get(Config::NUNCHUK_INPUT_SHAKE_INTENSITY_HARD),
m_shake_hard_step.data());
// Calibration values are 8-bit but we want 10-bit precision, so << 2.
auto acc = DenormalizeAccelData(accel, ACCEL_ZERO_G << 2, ACCEL_ONE_G << 2);
const auto acc = ConvertAccelData(accel, ACCEL_ZERO_G << 2, ACCEL_ONE_G << 2);
nc_data.ax = (acc.x >> 2) & 0xFF;
nc_data.ay = (acc.y >> 2) & 0xFF;
@ -145,6 +144,9 @@ void Nunchuk::Reset()
m_reg = {};
m_reg.identifier = nunchuk_id;
m_swing_state = {};
m_tilt_state = {};
// Build calibration data:
m_reg.calibration = {{
// Accel Zero X,Y,Z:
@ -195,6 +197,14 @@ ControllerEmu::ControlGroup* Nunchuk::GetGroup(NunchukGroup group)
}
}
void Nunchuk::DoState(PointerWrap& p)
{
EncryptedExtension::DoState(p);
p.Do(m_swing_state);
p.Do(m_tilt_state);
}
void Nunchuk::LoadDefaults(const ControllerInterface& ciface)
{
// Stick

View File

@ -7,6 +7,7 @@
#include <array>
#include "Core/HW/WiimoteCommon/WiimoteReport.h"
#include "Core/HW/WiimoteEmu/Dynamics.h"
#include "Core/HW/WiimoteEmu/Extension/Extension.h"
namespace ControllerEmu
@ -73,6 +74,7 @@ public:
void Update() override;
bool IsButtonPressed() const override;
void Reset() override;
void DoState(PointerWrap& p) override;
ControllerEmu::ControlGroup* GetGroup(NunchukGroup group);
@ -101,8 +103,6 @@ private:
ControllerEmu::Tilt* m_tilt;
ControllerEmu::Force* m_swing;
ControllerEmu::Force* m_swing_slow;
ControllerEmu::Force* m_swing_fast;
ControllerEmu::Buttons* m_shake;
ControllerEmu::Buttons* m_shake_soft;
@ -111,6 +111,11 @@ private:
ControllerEmu::Buttons* m_buttons;
ControllerEmu::AnalogStick* m_stick;
// Dynamics:
MotionState m_swing_state;
RotationalState m_tilt_state;
// TODO: kill
std::array<u8, 3> m_shake_step{};
std::array<u8, 3> m_shake_soft_step{};
std::array<u8, 3> m_shake_hard_step{};

View File

@ -64,9 +64,6 @@ static const char* const named_buttons[] = {
void Wiimote::Reset()
{
// TODO: This value should be re-read if SYSCONF gets changed.
m_sensor_bar_on_top = Config::Get(Config::SYSCONF_SENSOR_BAR_POSITION) != 0;
SetRumble(false);
// Wiimote starts in non-continuous CORE mode:
@ -133,10 +130,12 @@ void Wiimote::Reset()
m_status.extension = m_extension_port.IsDeviceConnected();
// Dynamics:
m_swing_state = {};
m_tilt_state = {};
m_shake_step = {};
m_shake_soft_step = {};
m_shake_hard_step = {};
m_swing_dynamic_data = {};
m_shake_dynamic_data = {};
}
@ -157,9 +156,6 @@ Wiimote::Wiimote(const unsigned int index) : m_index(index)
// swing
groups.emplace_back(m_swing = new ControllerEmu::Force(_trans("Swing")));
groups.emplace_back(m_swing_slow = new ControllerEmu::Force("SwingSlow"));
groups.emplace_back(m_swing_fast = new ControllerEmu::Force("SwingFast"));
groups.emplace_back(m_swing_dynamic = new ControllerEmu::Force("Swing Dynamic"));
// tilt
groups.emplace_back(m_tilt = new ControllerEmu::Tilt(_trans("Tilt")));
@ -247,6 +243,13 @@ Wiimote::Wiimote(const unsigned int index) : m_index(index)
m_hotkeys->AddInput(_trans("Sideways Hold"), false);
m_hotkeys->AddInput(_trans("Upright Hold"), false);
auto config_change_callback = [this] {
m_sensor_bar_on_top = Config::Get(Config::SYSCONF_SENSOR_BAR_POSITION) != 0;
};
Config::AddConfigChangedCallback(config_change_callback);
config_change_callback();
Reset();
}
@ -349,11 +352,18 @@ void Wiimote::Update()
if (0 == m_reporting_channel)
return;
const auto lock = GetStateLock();
// Hotkey / settings modifier
// Data is later accessed in IsSideways and IsUpright
m_hotkeys->GetState();
StepDynamics();
// Update buttons in the status struct which is sent in 99% of input reports.
// FYI: Movies only sync button updates in data reports.
if (!Core::WantsDeterminism())
{
const auto lock = GetStateLock();
UpdateButtonsStatus();
}
@ -405,14 +415,7 @@ void Wiimote::SendDataReport()
}
else
{
const auto lock = GetStateLock();
// Hotkey / settings modifier
// Data is later accessed in IsSideways and IsUpright
m_hotkeys->GetState();
// CORE
// Core buttons:
if (rpt_builder.HasCore())
{
if (Core::WantsDeterminism())
@ -424,26 +427,19 @@ void Wiimote::SendDataReport()
rpt_builder.SetCoreData(m_status.buttons);
}
// ACCEL
// FYI: This data is also used to tilt the IR dots.
NormalizedAccelData norm_accel = {};
GetAccelData(&norm_accel);
// Acceleration:
if (rpt_builder.HasAccel())
{
// Calibration values are 8-bit but we want 10-bit precision, so << 2.
DataReportBuilder::AccelData accel =
DenormalizeAccelData(norm_accel, ACCEL_ZERO_G << 2, ACCEL_ONE_G << 2);
ConvertAccelData(GetAcceleration(), ACCEL_ZERO_G << 2, ACCEL_ONE_G << 2);
rpt_builder.SetAccelData(accel);
}
// IR
// IR Camera:
if (rpt_builder.HasIR())
{
const auto cursor = m_ir->GetState(true);
m_camera_logic.Update(cursor, norm_accel, m_sensor_bar_on_top);
m_camera_logic.Update(GetTransformation(), m_sensor_bar_on_top);
// The real wiimote reads camera data from the i2c bus starting at offset 0x37:
const u8 camera_data_offset =
@ -453,8 +449,7 @@ void Wiimote::SendDataReport()
rpt_builder.GetIRDataPtr());
}
// EXT
// Extension port:
if (rpt_builder.HasExt())
{
// Update extension first as motion-plus may read from it.
@ -707,7 +702,67 @@ bool Wiimote::IsUpright() const
void Wiimote::SetRumble(bool on)
{
const auto lock = GetStateLock();
m_motor->control_ref->State(on);
}
void Wiimote::StepDynamics()
{
EmulateSwing(&m_swing_state, m_swing, 1.f / ::Wiimote::UPDATE_FREQ);
EmulateTilt(&m_tilt_state, m_tilt, 1.f / ::Wiimote::UPDATE_FREQ);
// TODO: Move cursor state out of ControllerEmu::Cursor
// const auto cursor_mtx = EmulateCursorMovement(m_ir);
}
Common::Vec3 Wiimote::GetAcceleration()
{
// Includes effects of:
// IR, Tilt, Swing, Orientation, Shake
auto orientation = Common::Matrix33::Identity();
if (IsSideways())
orientation *= Common::Matrix33::RotateZ(float(MathUtil::TAU / -4));
if (IsUpright())
orientation *= Common::Matrix33::RotateX(float(MathUtil::TAU / 4));
Common::Vec3 accel =
orientation *
GetTransformation().Transform(
m_swing_state.acceleration + Common::Vec3(0, 0, float(GRAVITY_ACCELERATION)), 0);
DynamicConfiguration shake_config;
shake_config.low_intensity = Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_SOFT);
shake_config.med_intensity = Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_MEDIUM);
shake_config.high_intensity = Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_HARD);
shake_config.frames_needed_for_high_intensity =
Config::Get(Config::WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_HARD);
shake_config.frames_needed_for_low_intensity =
Config::Get(Config::WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_HELD_SOFT);
shake_config.frames_to_execute = Config::Get(Config::WIIMOTE_INPUT_SHAKE_DYNAMIC_FRAMES_LENGTH);
accel += EmulateShake(m_shake, Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_MEDIUM),
m_shake_step.data());
accel += EmulateShake(m_shake_soft, Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_SOFT),
m_shake_soft_step.data());
accel += EmulateShake(m_shake_hard, Config::Get(Config::WIIMOTE_INPUT_SHAKE_INTENSITY_HARD),
m_shake_hard_step.data());
accel += EmulateDynamicShake(m_shake_dynamic_data, m_shake_dynamic, shake_config,
m_shake_dynamic_step.data());
return accel;
}
Common::Matrix44 Wiimote::GetTransformation() const
{
// Includes positional and rotational effects of:
// IR, Swing, Tilt
return Common::Matrix44::FromMatrix33(GetRotationalMatrix(-m_tilt_state.angle) *
GetRotationalMatrix(-m_swing_state.angle)) *
EmulateCursorMovement(m_ir) * Common::Matrix44::Translate(-m_swing_state.position);
}
} // namespace WiimoteEmu

View File

@ -125,6 +125,7 @@ public:
ControllerEmu::ControlGroup* GetTurntableGroup(TurntableGroup group);
void Update();
void StepDynamics();
void InterruptChannel(u16 channel_id, const void* data, u32 size);
void ControlChannel(u16 channel_id, const void* data, u32 size);
@ -146,7 +147,9 @@ private:
void UpdateButtonsStatus();
void GetAccelData(NormalizedAccelData* accel);
Common::Vec3 GetAcceleration();
// Used for simulating camera data. Does not include orientation transformations.
Common::Matrix44 GetTransformation() const;
void HIDOutputReport(const void* data, u32 size);
@ -238,9 +241,6 @@ private:
ControllerEmu::Cursor* m_ir;
ControllerEmu::Tilt* m_tilt;
ControllerEmu::Force* m_swing;
ControllerEmu::Force* m_swing_slow;
ControllerEmu::Force* m_swing_fast;
ControllerEmu::Force* m_swing_dynamic;
ControllerEmu::ControlGroup* m_rumble;
ControllerEmu::Output* m_motor;
ControllerEmu::Attachments* m_attachments;
@ -281,11 +281,14 @@ private:
UsableEEPROMData m_eeprom;
// Dynamics:
MotionState m_swing_state;
RotationalState m_tilt_state;
// TODO: kill these:
std::array<u8, 3> m_shake_step{};
std::array<u8, 3> m_shake_soft_step{};
std::array<u8, 3> m_shake_hard_step{};
std::array<u8, 3> m_shake_dynamic_step{};
DynamicData m_swing_dynamic_data;
DynamicData m_shake_dynamic_data;
};
} // namespace WiimoteEmu

View File

@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
static std::thread g_save_thread;
// Don't forget to increase this after doing changes on the savestate system
static const u32 STATE_VERSION = 103; // Last changed in PR 7674
static const u32 STATE_VERSION = 104; // Last changed in PR 7806
// Maps savestate versions to Dolphin versions.
// Versions after 42 don't need to be added to this list,

View File

@ -18,6 +18,7 @@
#include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/Control/Control.h"
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
#include "InputCommon/ControllerEmu/ControlGroup/Force.h"
#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h"
#include "InputCommon/ControllerEmu/Setting/NumericSetting.h"
#include "InputCommon/ControllerInterface/Device.h"
@ -45,16 +46,19 @@ const QColor STICK_GATE_COLOR = Qt::lightGray;
const QColor C_STICK_GATE_COLOR = Qt::yellow;
const QColor CURSOR_TV_COLOR = 0xaed6f1;
const QColor TILT_GATE_COLOR = 0xa2d9ce;
const QColor SWING_GATE_COLOR = 0xcea2d9;
constexpr int INPUT_DOT_RADIUS = 2;
constexpr int INDICATOR_UPDATE_FREQ = 30;
MappingIndicator::MappingIndicator(ControllerEmu::ControlGroup* group) : m_group(group)
{
setMinimumHeight(128);
const auto timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [this] { repaint(); });
timer->start(1000 / 30);
timer->start(1000 / INDICATOR_UPDATE_FREQ);
}
namespace
@ -250,7 +254,19 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
// We should probably hold the mutex for UI updates.
Settings::Instance().SetControllerStateNeeded(true);
const auto raw_coord = stick.GetReshapableState(false);
const auto adj_coord = stick.GetReshapableState(true);
Common::DVec2 adj_coord;
if (is_tilt)
{
WiimoteEmu::EmulateTilt(&m_motion_state, static_cast<ControllerEmu::Tilt*>(&stick),
1.f / INDICATOR_UPDATE_FREQ);
adj_coord = Common::DVec2{-m_motion_state.angle.y, m_motion_state.angle.x} / MathUtil::PI;
}
else
{
adj_coord = stick.GetReshapableState(true);
}
Settings::Instance().SetControllerStateNeeded(false);
UpdateCalibrationWidget(raw_coord);
@ -408,6 +424,100 @@ void MappingIndicator::DrawMixedTriggers()
}
}
void MappingIndicator::DrawForce(ControllerEmu::Force& force)
{
const QColor gate_brush_color = SWING_GATE_COLOR;
const QColor gate_pen_color = gate_brush_color.darker(125);
// TODO: This SetControllerStateNeeded interface leaks input into the game
// We should probably hold the mutex for UI updates.
Settings::Instance().SetControllerStateNeeded(true);
const auto raw_coord = force.GetState(false);
WiimoteEmu::EmulateSwing(&m_motion_state, &force, 1.f / INDICATOR_UPDATE_FREQ);
const auto& adj_coord = m_motion_state.position;
Settings::Instance().SetControllerStateNeeded(false);
UpdateCalibrationWidget({raw_coord.x, raw_coord.y});
// Bounding box size:
const double scale = height() / 2.5;
QPainter p(this);
p.translate(width() / 2, height() / 2);
// Bounding box.
p.setBrush(BBOX_BRUSH_COLOR);
p.setPen(BBOX_PEN_COLOR);
p.drawRect(-scale - 1, -scale - 1, scale * 2 + 1, scale * 2 + 1);
// UI y-axis is opposite that of stick.
p.scale(1.0, -1.0);
// Enable AA after drawing bounding box.
p.setRenderHint(QPainter::Antialiasing, true);
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
if (IsCalibrating())
{
DrawCalibration(p, {raw_coord.x, raw_coord.y});
return;
}
// Deadzone for Z (forward/backward):
const double deadzone = force.numeric_settings[force.SETTING_DEADZONE]->GetValue();
if (deadzone > 0.0)
{
p.setPen(DEADZONE_COLOR);
p.setBrush(DEADZONE_BRUSH);
p.drawRect(QRectF(-scale, -deadzone * scale, scale * 2, deadzone * scale * 2));
}
// Raw Z:
p.setPen(Qt::NoPen);
p.setBrush(RAW_INPUT_COLOR);
p.drawRect(
QRectF(-scale, raw_coord.z * scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS));
// Adjusted Z:
if (adj_coord.y)
{
p.setBrush(ADJ_INPUT_COLOR);
p.drawRect(
QRectF(-scale, adj_coord.y * -scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS));
}
// Draw "gate" shape.
p.setPen(gate_pen_color);
p.setBrush(gate_brush_color);
p.drawPolygon(GetPolygonFromRadiusGetter(
[&force](double ang) { return force.GetGateRadiusAtAngle(ang); }, scale));
// Deadzone.
p.setPen(DEADZONE_COLOR);
p.setBrush(DEADZONE_BRUSH);
p.drawPolygon(GetPolygonFromRadiusGetter(
[&force](double ang) { return force.GetDeadzoneRadiusAtAngle(ang); }, scale));
// Input shape.
p.setPen(INPUT_SHAPE_PEN);
p.setBrush(Qt::NoBrush);
p.drawPolygon(GetPolygonFromRadiusGetter(
[&force](double ang) { return force.GetInputRadiusAtAngle(ang); }, scale));
// Raw stick position.
p.setPen(Qt::NoPen);
p.setBrush(RAW_INPUT_COLOR);
p.drawEllipse(QPointF{raw_coord.x, raw_coord.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
// Adjusted position:
if (adj_coord.x || adj_coord.z)
{
p.setPen(Qt::NoPen);
p.setBrush(ADJ_INPUT_COLOR);
p.drawEllipse(QPointF{-adj_coord.x, adj_coord.z} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
}
}
void MappingIndicator::paintEvent(QPaintEvent*)
{
switch (m_group->type)
@ -422,6 +532,9 @@ void MappingIndicator::paintEvent(QPaintEvent*)
case ControllerEmu::GroupType::MixedTriggers:
DrawMixedTriggers();
break;
case ControllerEmu::GroupType::Force:
DrawForce(*static_cast<ControllerEmu::Force*>(m_group));
break;
default:
break;
}

View File

@ -7,6 +7,7 @@
#include <QToolButton>
#include <QWidget>
#include "Core/HW/WiimoteEmu/Dynamics.h"
#include "InputCommon/ControllerEmu/StickGate.h"
namespace ControllerEmu
@ -14,6 +15,7 @@ namespace ControllerEmu
class Control;
class ControlGroup;
class Cursor;
class Force;
class NumericSetting;
} // namespace ControllerEmu
@ -34,6 +36,7 @@ private:
void DrawCursor(ControllerEmu::Cursor& cursor);
void DrawReshapableInput(ControllerEmu::ReshapableInput& stick);
void DrawMixedTriggers();
void DrawForce(ControllerEmu::Force&);
void DrawCalibration(QPainter& p, Common::DVec2 point);
void paintEvent(QPaintEvent*) override;
@ -43,6 +46,8 @@ private:
ControllerEmu::ControlGroup* const m_group;
CalibrationWidget* m_calibration_widget{};
WiimoteEmu::MotionState m_motion_state{};
};
class CalibrationWidget : public QToolButton

View File

@ -77,11 +77,13 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
const bool need_indicator = group->type == ControllerEmu::GroupType::Cursor ||
group->type == ControllerEmu::GroupType::Stick ||
group->type == ControllerEmu::GroupType::Tilt ||
group->type == ControllerEmu::GroupType::MixedTriggers;
group->type == ControllerEmu::GroupType::MixedTriggers ||
group->type == ControllerEmu::GroupType::Force;
const bool need_calibration = group->type == ControllerEmu::GroupType::Cursor ||
group->type == ControllerEmu::GroupType::Stick ||
group->type == ControllerEmu::GroupType::Tilt;
group->type == ControllerEmu::GroupType::Tilt ||
group->type == ControllerEmu::GroupType::Force;
for (auto& control : group->controls)
{

View File

@ -4,12 +4,11 @@
#include "InputCommon/ControllerEmu/ControlGroup/Force.h"
#include <cmath>
#include <memory>
#include <string>
#include "Common/Common.h"
#include "Common/CommonTypes.h"
#include "Common/MathUtil.h"
#include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/Control/Input.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
@ -17,7 +16,7 @@
namespace ControllerEmu
{
Force::Force(const std::string& name_) : ControlGroup(name_, GroupType::Force)
Force::Force(const std::string& name_) : ReshapableInput(name_, name_, GroupType::Force)
{
controls.emplace_back(std::make_unique<Input>(Translate, _trans("Up")));
controls.emplace_back(std::make_unique<Input>(Translate, _trans("Down")));
@ -26,26 +25,69 @@ Force::Force(const std::string& name_) : ControlGroup(name_, GroupType::Force)
controls.emplace_back(std::make_unique<Input>(Translate, _trans("Forward")));
controls.emplace_back(std::make_unique<Input>(Translate, _trans("Backward")));
numeric_settings.emplace_back(std::make_unique<NumericSetting>(_trans("Dead Zone"), 0, 0, 50));
// Maximum swing movement (centimeters).
numeric_settings.emplace_back(std::make_unique<NumericSetting>(_trans("Distance"), 0.25, 1, 100));
// Maximum jerk (m/s^3).
// i18n: "Jerk" as it relates to physics. The time derivative of acceleration.
numeric_settings.emplace_back(std::make_unique<NumericSetting>(_trans("Jerk"), 5.0, 1, 1000));
// Angle of twist applied at the extremities of the swing (degrees).
numeric_settings.emplace_back(std::make_unique<NumericSetting>(_trans("Angle"), 0.45, 0, 180));
}
Force::StateData Force::GetState()
Force::ReshapeData Force::GetReshapableState(bool adjusted)
{
StateData state_data;
const ControlState deadzone = numeric_settings[0]->GetValue();
const ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State();
const ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State();
for (u32 i = 0; i < 6; i += 2)
// Return raw values. (used in UI)
if (!adjusted)
return {x, y};
return Reshape(x, y);
}
Force::StateData Force::GetState(bool adjusted)
{
const auto state = GetReshapableState(adjusted);
ControlState z = controls[4]->control_ref->State() - controls[5]->control_ref->State();
if (adjusted)
{
const ControlState state =
controls[i + 1]->control_ref->State() - controls[i]->control_ref->State();
ControlState tmpf = 0;
if (fabs(state) > deadzone)
tmpf = ((state - (deadzone * sign(state))) / (1 - deadzone));
state_data[i / 2] = tmpf;
// Apply deadzone to z.
const ControlState deadzone = numeric_settings[SETTING_DEADZONE]->GetValue();
z = std::copysign(std::max(0.0, std::abs(z) - deadzone) / (1.0 - deadzone), z);
}
return state_data;
return {float(state.x), float(state.y), float(z)};
}
ControlState Force::GetGateRadiusAtAngle(double) const
{
// Just a circle of the configured distance:
return numeric_settings[SETTING_DISTANCE]->GetValue();
}
ControlState Force::GetMaxJerk() const
{
return numeric_settings[SETTING_JERK]->GetValue() * 100;
}
ControlState Force::GetTwistAngle() const
{
return numeric_settings[SETTING_ANGLE]->GetValue() * MathUtil::TAU / 3.60;
}
ControlState Force::GetMaxDistance() const
{
return numeric_settings[SETTING_DISTANCE]->GetValue();
}
ControlState Force::GetDefaultInputRadiusAtAngle(double) const
{
// Just a circle of radius 1.0.
return 1.0;
}
} // namespace ControllerEmu

View File

@ -6,18 +6,41 @@
#include <array>
#include <string>
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
#include "InputCommon/ControllerInterface/Device.h"
#include "Common/Matrix.h"
#include "InputCommon/ControllerEmu/StickGate.h"
namespace ControllerEmu
{
class Force : public ControlGroup
class Force : public ReshapableInput
{
public:
using StateData = std::array<ControlState, 3>;
using StateData = Common::Vec3;
explicit Force(const std::string& name);
StateData GetState();
ReshapeData GetReshapableState(bool adjusted) final override;
ControlState GetGateRadiusAtAngle(double ang) const final override;
ControlState GetDefaultInputRadiusAtAngle(double angle) const final override;
StateData GetState(bool adjusted = true);
// Return jerk in m/s^3.
ControlState GetMaxJerk() const;
// Return twist angle in radians.
ControlState GetTwistAngle() const;
// Return swing distance in meters.
ControlState GetMaxDistance() const;
private:
enum
{
SETTING_DISTANCE = ReshapableInput::SETTING_COUNT,
SETTING_JERK,
SETTING_ANGLE,
};
};
} // namespace ControllerEmu

View File

@ -4,13 +4,9 @@
#include "InputCommon/ControllerEmu/ControlGroup/Tilt.h"
#include <algorithm>
#include <cmath>
#include <memory>
#include <string>
#include "Common/Common.h"
#include "Common/MathUtil.h"
#include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/Control/Control.h"
@ -19,8 +15,7 @@
namespace ControllerEmu
{
Tilt::Tilt(const std::string& name_)
: ReshapableInput(name_, name_, GroupType::Tilt), m_last_update(Clock::now())
Tilt::Tilt(const std::string& name_) : ReshapableInput(name_, name_, GroupType::Tilt)
{
controls.emplace_back(std::make_unique<Input>(Translate, _trans("Forward")));
controls.emplace_back(std::make_unique<Input>(Translate, _trans("Backward")));
@ -43,33 +38,7 @@ Tilt::ReshapeData Tilt::GetReshapableState(bool adjusted)
const ControlState modifier = controls[4]->control_ref->State();
// Compute desired tilt:
StateData target = Reshape(x, y, modifier);
// Step the simulation. This is somewhat ugly being here.
// We should be able to GetState without changing state.
// State should be stored outside of this object inside the wiimote,
// and separately inside the UI.
// We're using system time rather than ticks to step this.
// I don't think that's too horrible as we can consider this part of user input.
// And at least the Mapping UI will behave sanely this way.
// TODO: when state is moved outside of this class have a separate Step()
// function that takes a ms_passed argument
const auto now = Clock::now();
const auto ms_since_update =
std::chrono::duration_cast<std::chrono::milliseconds>(now - m_last_update).count();
m_last_update = now;
const double max_step = MAX_DEG_PER_SEC / 180.0 * ms_since_update / 1000;
// TODO: Allow wrap around from 1.0 to -1.0
// (take the fastest route to target)
m_tilt.x += MathUtil::Clamp(target.x - m_tilt.x, -max_step, max_step);
m_tilt.y += MathUtil::Clamp(target.y - m_tilt.y, -max_step, max_step);
return m_tilt;
return Reshape(x, y, modifier);
}
Tilt::StateData Tilt::GetState()

View File

@ -4,7 +4,6 @@
#pragma once
#include <chrono>
#include <string>
#include "InputCommon/ControllerEmu/StickGate.h"
@ -33,12 +32,5 @@ private:
{
SETTING_MAX_ANGLE = ReshapableInput::SETTING_COUNT,
};
static constexpr int MAX_DEG_PER_SEC = 360 * 6;
StateData m_tilt;
using Clock = std::chrono::steady_clock;
Clock::time_point m_last_update;
};
} // namespace ControllerEmu