|
@@ -30,6 +30,217 @@
|
|
|
#include "gamepad_wired.h"
|
|
|
#include "gamepad_wireless.h"
|
|
|
|
|
|
+#include <limits.h>
|
|
|
+
|
|
|
+#define RAD_TO_DEG (180.0f / SDL_PI_F)
|
|
|
+
|
|
|
+/* Used to draw a 3D cube to represent the gyroscope orientation */
|
|
|
+typedef struct
|
|
|
+{
|
|
|
+ float x, y, z;
|
|
|
+} Vector3;
|
|
|
+
|
|
|
+struct Quaternion
|
|
|
+{
|
|
|
+ float x, y, z, w;
|
|
|
+};
|
|
|
+
|
|
|
+static const Vector3 debug_cube_vertices[] = {
|
|
|
+ { -1.0f, -1.0f, -1.0f },
|
|
|
+ { 1.0f, -1.0f, -1.0f },
|
|
|
+ { 1.0f, 1.0f, -1.0f },
|
|
|
+ { -1.0f, 1.0f, -1.0f },
|
|
|
+ { -1.0f, -1.0f, 1.0f },
|
|
|
+ { 1.0f, -1.0f, 1.0f },
|
|
|
+ { 1.0f, 1.0f, 1.0f },
|
|
|
+ { -1.0f, 1.0f, 1.0f },
|
|
|
+};
|
|
|
+
|
|
|
+static const int debug_cube_edges[][2] = {
|
|
|
+ { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 0 }, /* bottom square */
|
|
|
+ { 4, 5 }, { 5, 6 }, { 6, 7 }, { 7, 4 }, /* top square */
|
|
|
+ { 0, 4 }, { 1, 5 }, { 2, 6 }, { 3, 7 }, /* verticals */
|
|
|
+};
|
|
|
+
|
|
|
+static Vector3 RotateVectorByQuaternion(const Vector3 *v, const Quaternion *q) {
|
|
|
+ /* v' = q * v * q^-1 */
|
|
|
+ float x = v->x, y = v->y, z = v->z;
|
|
|
+ float qx = q->x, qy = q->y, qz = q->z, qw = q->w;
|
|
|
+
|
|
|
+ /* Calculate quaternion *vector */
|
|
|
+ float ix = qw * x + qy * z - qz * y;
|
|
|
+ float iy = qw * y + qz * x - qx * z;
|
|
|
+ float iz = qw * z + qx * y - qy * x;
|
|
|
+ float iw = -qx * x - qy * y - qz * z;
|
|
|
+
|
|
|
+ /* Result = result * conjugate(q) */
|
|
|
+ Vector3 out;
|
|
|
+ out.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
|
|
|
+ out.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
|
|
|
+ out.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+#ifdef GYRO_ISOMETRIC_PROJECTION
|
|
|
+static SDL_FPoint ProjectVec3ToRect(const Vector3 *v, const SDL_FRect *rect)
|
|
|
+{
|
|
|
+ SDL_FPoint out;
|
|
|
+ /* Simple orthographic projection using X and Y; scale to fit into rect */
|
|
|
+ out.x = rect->x + (rect->w / 2.0f) + (v->x * (rect->w / 2.0f));
|
|
|
+ out.y = rect->y + (rect->h / 2.0f) - (v->y * (rect->h / 2.0f)); /* Y inverted */
|
|
|
+ return out;
|
|
|
+}
|
|
|
+#else
|
|
|
+static SDL_FPoint ProjectVec3ToRect(const Vector3 *v, const SDL_FRect *rect)
|
|
|
+{
|
|
|
+ const float verticalFOV_deg = 40.0f;
|
|
|
+ const float cameraZ = 4.0f; /* Camera is at(0, 0, +4), looking toward origin */
|
|
|
+ float aspect = rect->w / rect->h;
|
|
|
+
|
|
|
+ float fovScaleY = SDL_tanf((verticalFOV_deg * SDL_PI_F / 180.0f) * 0.5f);
|
|
|
+ float fovScaleX = fovScaleY * aspect;
|
|
|
+
|
|
|
+ float relZ = cameraZ - v->z;
|
|
|
+ if (relZ < 0.01f)
|
|
|
+ relZ = 0.01f; /* Prevent division by 0 or negative depth */
|
|
|
+
|
|
|
+ float ndc_x = (v->x / relZ) / fovScaleX;
|
|
|
+ float ndc_y = (v->y / relZ) / fovScaleY;
|
|
|
+
|
|
|
+ /* Convert to screen space */
|
|
|
+ SDL_FPoint out;
|
|
|
+ out.x = rect->x + (rect->w / 2.0f) + (ndc_x * rect->w / 2.0f);
|
|
|
+ out.y = rect->y + (rect->h / 2.0f) - (ndc_y * rect->h / 2.0f); /* flip Y */
|
|
|
+ return out;
|
|
|
+}
|
|
|
+#endif
|
|
|
+
|
|
|
+void DrawGyroDebugCube(SDL_Renderer *renderer, const Quaternion *orientation, const SDL_FRect *rect)
|
|
|
+{
|
|
|
+ SDL_FPoint projected[8];
|
|
|
+ int i;
|
|
|
+ for (i = 0; i < 8; ++i) {
|
|
|
+ Vector3 rotated = RotateVectorByQuaternion(&debug_cube_vertices[i], orientation);
|
|
|
+ projected[i] = ProjectVec3ToRect(&rotated, rect);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (i = 0; i < 12; ++i) {
|
|
|
+ const SDL_FPoint p0 = projected[debug_cube_edges[i][0]];
|
|
|
+ const SDL_FPoint p1 = projected[debug_cube_edges[i][1]];
|
|
|
+ SDL_RenderLine(renderer, p0.x, p0.y, p1.x, p1.y);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#define CIRCLE_SEGMENTS 64
|
|
|
+
|
|
|
+static Vector3 kCirclePoints3D_XY_Plane[CIRCLE_SEGMENTS];
|
|
|
+static Vector3 kCirclePoints3D_XZ_Plane[CIRCLE_SEGMENTS];
|
|
|
+static Vector3 kCirclePoints3D_YZ_Plane[CIRCLE_SEGMENTS];
|
|
|
+
|
|
|
+void InitCirclePoints3D(void)
|
|
|
+{
|
|
|
+ int i;
|
|
|
+ for (i = 0; i < CIRCLE_SEGMENTS; ++i) {
|
|
|
+ float theta = ((float)i / CIRCLE_SEGMENTS) * SDL_PI_F * 2.0f;
|
|
|
+ kCirclePoints3D_XY_Plane[i].x = SDL_cosf(theta);
|
|
|
+ kCirclePoints3D_XY_Plane[i].y = SDL_sinf(theta);
|
|
|
+ kCirclePoints3D_XY_Plane[i].z = 0.0f;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (i = 0; i < CIRCLE_SEGMENTS; ++i) {
|
|
|
+ float theta = ((float)i / CIRCLE_SEGMENTS) * SDL_PI_F * 2.0f;
|
|
|
+ kCirclePoints3D_XZ_Plane[i].x = SDL_cosf(theta);
|
|
|
+ kCirclePoints3D_XZ_Plane[i].y = 0.0f;
|
|
|
+ kCirclePoints3D_XZ_Plane[i].z = SDL_sinf(theta);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (i = 0; i < CIRCLE_SEGMENTS; ++i) {
|
|
|
+ float theta = ((float)i / CIRCLE_SEGMENTS) * SDL_PI_F * 2.0f;
|
|
|
+ kCirclePoints3D_YZ_Plane[i].x = 0.0f;
|
|
|
+ kCirclePoints3D_YZ_Plane[i].y = SDL_cosf(theta);
|
|
|
+ kCirclePoints3D_YZ_Plane[i].z = SDL_sinf(theta);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void DrawGyroCircle(
|
|
|
+ SDL_Renderer *renderer,
|
|
|
+ const Vector3 *circlePoints,
|
|
|
+ int numSegments,
|
|
|
+ const Quaternion *orientation,
|
|
|
+ const SDL_FRect *bounds,
|
|
|
+ Uint8 r, Uint8 g, Uint8 b, Uint8 a)
|
|
|
+{
|
|
|
+ SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
|
|
+
|
|
|
+ SDL_FPoint lastScreenPt = { 0 };
|
|
|
+ bool hasLast = false;
|
|
|
+ int i;
|
|
|
+ for (i = 0; i <= numSegments; ++i) {
|
|
|
+ int index = i % numSegments;
|
|
|
+
|
|
|
+ Vector3 rotated = RotateVectorByQuaternion(&circlePoints[index], orientation);
|
|
|
+ SDL_FPoint screenPtVec2 = ProjectVec3ToRect(&rotated, bounds);
|
|
|
+ SDL_FPoint screenPt;
|
|
|
+ screenPt.x = screenPtVec2.x;
|
|
|
+ screenPt.y = screenPtVec2.y;
|
|
|
+
|
|
|
+
|
|
|
+ if (hasLast) {
|
|
|
+ SDL_RenderLine(renderer, lastScreenPt.x, lastScreenPt.y, screenPt.x, screenPt.y);
|
|
|
+ }
|
|
|
+
|
|
|
+ lastScreenPt = screenPt;
|
|
|
+ hasLast = true;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void DrawGyroDebugCircle(SDL_Renderer *renderer, const Quaternion *orientation, const SDL_FRect *bounds)
|
|
|
+{
|
|
|
+ /* Store current color */
|
|
|
+ Uint8 r, g, b, a;
|
|
|
+ SDL_GetRenderDrawColor(renderer, &r, &g, &b, &a);
|
|
|
+ DrawGyroCircle(renderer, kCirclePoints3D_YZ_Plane, CIRCLE_SEGMENTS, orientation, bounds, GYRO_COLOR_RED); /* X axis - pitch */
|
|
|
+ DrawGyroCircle(renderer, kCirclePoints3D_XZ_Plane, CIRCLE_SEGMENTS, orientation, bounds, GYRO_COLOR_GREEN); /* Y axis - yaw */
|
|
|
+ DrawGyroCircle(renderer, kCirclePoints3D_XY_Plane, CIRCLE_SEGMENTS, orientation, bounds, GYRO_COLOR_BLUE); /* Z axis - Roll */
|
|
|
+
|
|
|
+ /* Restore current color */
|
|
|
+ SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
|
|
+}
|
|
|
+
|
|
|
+void DrawAccelerometerDebugArrow(SDL_Renderer *renderer, const Quaternion *gyro_quaternion, const float *accel_data, const SDL_FRect *bounds)
|
|
|
+{
|
|
|
+ /* Store current color */
|
|
|
+ Uint8 r, g, b, a;
|
|
|
+ SDL_GetRenderDrawColor(renderer, &r, &g, &b, &a);
|
|
|
+
|
|
|
+ const float flGravity = 9.81f;
|
|
|
+ Vector3 vAccel;
|
|
|
+ vAccel.x = accel_data[0] / flGravity;
|
|
|
+ vAccel.y = accel_data[1] / flGravity;
|
|
|
+ vAccel.z = accel_data[2] / flGravity;
|
|
|
+
|
|
|
+ Vector3 origin = { 0.0f, 0.0f, 0.0f };
|
|
|
+ Vector3 rotated_accel = RotateVectorByQuaternion(&vAccel, gyro_quaternion);
|
|
|
+
|
|
|
+ /* Project the origin and rotated vector to screen space */
|
|
|
+ SDL_FPoint origin_screen = ProjectVec3ToRect(&origin, bounds);
|
|
|
+ SDL_FPoint accel_screen = ProjectVec3ToRect(&rotated_accel, bounds);
|
|
|
+
|
|
|
+ /* Draw the line from origin to the rotated accelerometer vector */
|
|
|
+ SDL_SetRenderDrawColor(renderer, GYRO_COLOR_ORANGE);
|
|
|
+ SDL_RenderLine(renderer, origin_screen.x, origin_screen.y, accel_screen.x, accel_screen.y);
|
|
|
+
|
|
|
+ const float head_width = 4.0f;
|
|
|
+ SDL_FRect arrow_head_rect;
|
|
|
+ arrow_head_rect.x = accel_screen.x - head_width * 0.5f;
|
|
|
+ arrow_head_rect.y = accel_screen.y - head_width * 0.5f;
|
|
|
+ arrow_head_rect.w = head_width;
|
|
|
+ arrow_head_rect.h = head_width;
|
|
|
+ SDL_RenderRect(renderer, &arrow_head_rect);
|
|
|
+
|
|
|
+ /* Restore current color */
|
|
|
+ SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
|
|
+}
|
|
|
|
|
|
/* This is indexed by gamepad element */
|
|
|
static const struct
|
|
@@ -683,7 +894,6 @@ void DestroyGamepadImage(GamepadImage *ctx)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-
|
|
|
static const char *gamepad_button_names[] = {
|
|
|
"South",
|
|
|
"East",
|
|
@@ -736,6 +946,8 @@ struct GamepadDisplay
|
|
|
|
|
|
float accel_data[3];
|
|
|
float gyro_data[3];
|
|
|
+ float gyro_drift_correction_data[3];
|
|
|
+
|
|
|
Uint64 last_sensor_update;
|
|
|
|
|
|
ControllerDisplayMode display_mode;
|
|
@@ -760,10 +972,68 @@ GamepadDisplay *CreateGamepadDisplay(SDL_Renderer *renderer)
|
|
|
|
|
|
ctx->element_highlighted = SDL_GAMEPAD_ELEMENT_INVALID;
|
|
|
ctx->element_selected = SDL_GAMEPAD_ELEMENT_INVALID;
|
|
|
+
|
|
|
+ SDL_zeroa(ctx->accel_data);
|
|
|
+ SDL_zeroa(ctx->gyro_data);
|
|
|
+ SDL_zeroa(ctx->gyro_drift_correction_data);
|
|
|
}
|
|
|
return ctx;
|
|
|
}
|
|
|
|
|
|
+struct GyroDisplay
|
|
|
+{
|
|
|
+ SDL_Renderer *renderer;
|
|
|
+
|
|
|
+ /* Main drawing area */
|
|
|
+ SDL_FRect area;
|
|
|
+
|
|
|
+ /* This part displays extra info from the IMUstate in order to figure out actual polling rates. */
|
|
|
+ float gyro_drift_solution[3];
|
|
|
+ int reported_sensor_rate_hz; /*hz - comes from HIDsdl implementation. Could be fixed, platform time, or true sensor time*/
|
|
|
+ int estimated_sensor_rate_hz; /*hz - our estimation of the actual polling rate by observing packets received*/
|
|
|
+ float euler_displacement_angles[3]; /* pitch, yaw, roll */
|
|
|
+ Quaternion gyro_quaternion; /* Rotation since startup/reset, comprised of each gyro speed packet times sensor delta time. */
|
|
|
+ float drift_calibration_progress_frac; /* [0..1] */
|
|
|
+ float accelerometer_noise_sq; /* Distance between last noise and new noise. Used to indicate motion.*/
|
|
|
+
|
|
|
+ GamepadButton *reset_gyro_button;
|
|
|
+ GamepadButton *calibrate_gyro_button;
|
|
|
+};
|
|
|
+
|
|
|
+GyroDisplay *CreateGyroDisplay(SDL_Renderer *renderer)
|
|
|
+{
|
|
|
+ GyroDisplay *ctx = SDL_calloc(1, sizeof(*ctx));
|
|
|
+ {
|
|
|
+ ctx->renderer = renderer;
|
|
|
+ ctx->estimated_sensor_rate_hz = 0;
|
|
|
+ SDL_zeroa(ctx->gyro_drift_solution);
|
|
|
+ Quaternion quat_identity = { 0.0f, 0.0f, 0.0f, 1.0f };
|
|
|
+ ctx->gyro_quaternion = quat_identity;
|
|
|
+
|
|
|
+ ctx->reset_gyro_button = CreateGamepadButton(renderer, "Reset View");
|
|
|
+ ctx->calibrate_gyro_button = CreateGamepadButton(renderer, "Recalibrate Drift");
|
|
|
+ }
|
|
|
+
|
|
|
+ return ctx;
|
|
|
+}
|
|
|
+
|
|
|
+void SetGyroDisplayArea(GyroDisplay *ctx, const SDL_FRect *area)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ SDL_copyp(&ctx->area, area);
|
|
|
+
|
|
|
+ /* Place the reset button to the bottom right of the gyro display area.*/
|
|
|
+ SDL_FRect reset_button_area;
|
|
|
+ reset_button_area.w = SDL_max(MINIMUM_BUTTON_WIDTH, GetGamepadButtonLabelWidth(ctx->reset_gyro_button) + 2 * BUTTON_PADDING);
|
|
|
+ reset_button_area.h = GetGamepadButtonLabelHeight(ctx->reset_gyro_button) + BUTTON_PADDING;
|
|
|
+ reset_button_area.x = area->x + area->w - reset_button_area.w - BUTTON_PADDING;
|
|
|
+ reset_button_area.y = area->y + area->h - reset_button_area.h - BUTTON_PADDING;
|
|
|
+ SetGamepadButtonArea(ctx->reset_gyro_button, &reset_button_area);
|
|
|
+}
|
|
|
+
|
|
|
void SetGamepadDisplayDisplayMode(GamepadDisplay *ctx, ControllerDisplayMode display_mode)
|
|
|
{
|
|
|
if (!ctx) {
|
|
@@ -781,6 +1051,16 @@ void SetGamepadDisplayArea(GamepadDisplay *ctx, const SDL_FRect *area)
|
|
|
|
|
|
SDL_copyp(&ctx->area, area);
|
|
|
}
|
|
|
+void SetGamepadDisplayGyroDriftCorrection(GamepadDisplay *ctx, float *gyro_drift_correction)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx->gyro_drift_correction_data[0] = gyro_drift_correction[0];
|
|
|
+ ctx->gyro_drift_correction_data[1] = gyro_drift_correction[1];
|
|
|
+ ctx->gyro_drift_correction_data[2] = gyro_drift_correction[2];
|
|
|
+}
|
|
|
|
|
|
static bool GetBindingString(const char *label, const char *mapping, char *text, size_t size)
|
|
|
{
|
|
@@ -1044,6 +1324,50 @@ static void RenderGamepadElementHighlight(GamepadDisplay *ctx, int element, cons
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+bool BHasCachedGyroDriftSolution(GyroDisplay *ctx)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return (ctx->gyro_drift_solution[0] != 0.0f ||
|
|
|
+ ctx->gyro_drift_solution[1] != 0.0f ||
|
|
|
+ ctx->gyro_drift_solution[2] != 0.0f);
|
|
|
+}
|
|
|
+
|
|
|
+void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, float *euler_displacement_angles, Quaternion *gyro_quaternion, int reported_senor_rate_hz, int estimated_sensor_rate_hz, float drift_calibration_progress_frac, float accelerometer_noise_sq)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ SDL_memcpy(ctx->gyro_drift_solution, gyro_drift_solution, sizeof(ctx->gyro_drift_solution));
|
|
|
+ ctx->estimated_sensor_rate_hz = estimated_sensor_rate_hz;
|
|
|
+
|
|
|
+ if (reported_senor_rate_hz != 0)
|
|
|
+ ctx->reported_sensor_rate_hz = reported_senor_rate_hz;
|
|
|
+
|
|
|
+ SDL_memcpy(ctx->euler_displacement_angles, euler_displacement_angles, sizeof(ctx->euler_displacement_angles));
|
|
|
+ ctx->gyro_quaternion = *gyro_quaternion;
|
|
|
+ ctx->drift_calibration_progress_frac = drift_calibration_progress_frac;
|
|
|
+ ctx->accelerometer_noise_sq = accelerometer_noise_sq;
|
|
|
+}
|
|
|
+
|
|
|
+extern GamepadButton *GetGyroResetButton(GyroDisplay *ctx)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return NULL;
|
|
|
+ }
|
|
|
+ return ctx->reset_gyro_button;
|
|
|
+}
|
|
|
+
|
|
|
+extern GamepadButton *GetGyroCalibrateButton(GyroDisplay *ctx)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return NULL;
|
|
|
+ }
|
|
|
+ return ctx->calibrate_gyro_button;
|
|
|
+}
|
|
|
+
|
|
|
void RenderGamepadDisplay(GamepadDisplay *ctx, SDL_Gamepad *gamepad)
|
|
|
{
|
|
|
float x, y;
|
|
@@ -1285,8 +1609,10 @@ void RenderGamepadDisplay(GamepadDisplay *ctx, SDL_Gamepad *gamepad)
|
|
|
|
|
|
has_accel = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL);
|
|
|
has_gyro = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO);
|
|
|
+
|
|
|
if (has_accel || has_gyro) {
|
|
|
- const int SENSOR_UPDATE_INTERVAL_MS = 100;
|
|
|
+ const float gyro_sensor_rate = has_gyro ? SDL_GetGamepadSensorDataRate(gamepad, SDL_SENSOR_GYRO) : 0;
|
|
|
+ const int SENSOR_UPDATE_INTERVAL_MS = gyro_sensor_rate > 0.0f ? (int)( 1000.0f / gyro_sensor_rate ) : 100;
|
|
|
Uint64 now = SDL_GetTicks();
|
|
|
|
|
|
if (now >= ctx->last_sensor_update + SENSOR_UPDATE_INTERVAL_MS) {
|
|
@@ -1296,26 +1622,37 @@ void RenderGamepadDisplay(GamepadDisplay *ctx, SDL_Gamepad *gamepad)
|
|
|
if (has_gyro) {
|
|
|
SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_GYRO, ctx->gyro_data, SDL_arraysize(ctx->gyro_data));
|
|
|
}
|
|
|
- ctx->last_sensor_update = now;
|
|
|
}
|
|
|
|
|
|
if (has_accel) {
|
|
|
SDL_strlcpy(text, "Accelerometer:", sizeof(text));
|
|
|
SDLTest_DrawString(ctx->renderer, x + center - SDL_strlen(text) * FONT_CHARACTER_SIZE, y, text);
|
|
|
- SDL_snprintf(text, sizeof(text), "(%.2f,%.2f,%.2f)", ctx->accel_data[0], ctx->accel_data[1], ctx->accel_data[2]);
|
|
|
+ SDL_snprintf(text, sizeof(text), "[%.2f,%.2f,%.2f]m/s%s", ctx->accel_data[0], ctx->accel_data[1], ctx->accel_data[2], SQUARED_UTF8 );
|
|
|
SDLTest_DrawString(ctx->renderer, x + center + 2.0f, y, text);
|
|
|
-
|
|
|
y += ctx->button_height + 2.0f;
|
|
|
}
|
|
|
|
|
|
if (has_gyro) {
|
|
|
SDL_strlcpy(text, "Gyro:", sizeof(text));
|
|
|
SDLTest_DrawString(ctx->renderer, x + center - SDL_strlen(text) * FONT_CHARACTER_SIZE, y, text);
|
|
|
- SDL_snprintf(text, sizeof(text), "(%.2f,%.2f,%.2f)", ctx->gyro_data[0], ctx->gyro_data[1], ctx->gyro_data[2]);
|
|
|
+ SDL_snprintf(text, sizeof(text), "[%.2f,%.2f,%.2f]%s/s", ctx->gyro_data[0] * RAD_TO_DEG, ctx->gyro_data[1] * RAD_TO_DEG, ctx->gyro_data[2] * RAD_TO_DEG, DEGREE_UTF8);
|
|
|
SDLTest_DrawString(ctx->renderer, x + center + 2.0f, y, text);
|
|
|
+
|
|
|
+
|
|
|
+ /* Display a smoothed version of the above for the sake of turntable tests */
|
|
|
+
|
|
|
+ if (ctx->gyro_drift_correction_data[0] != 0.0f && ctx->gyro_drift_correction_data[2] != 0.0f && ctx->gyro_drift_correction_data[2] != 0.0f )
|
|
|
+ {
|
|
|
+ y += ctx->button_height + 2.0f;
|
|
|
+ SDL_strlcpy(text, "Gyro Drift:", sizeof(text));
|
|
|
+ SDLTest_DrawString(ctx->renderer, x + center - SDL_strlen(text) * FONT_CHARACTER_SIZE, y, text);
|
|
|
+ SDL_snprintf(text, sizeof(text), "[%.2f,%.2f,%.2f]%s/s", ctx->gyro_drift_correction_data[0] * RAD_TO_DEG, ctx->gyro_drift_correction_data[1] * RAD_TO_DEG, ctx->gyro_drift_correction_data[2] * RAD_TO_DEG, DEGREE_UTF8);
|
|
|
+ SDLTest_DrawString(ctx->renderer, x + center + 2.0f, y, text);
|
|
|
+ }
|
|
|
|
|
|
- y += ctx->button_height + 2.0f;
|
|
|
}
|
|
|
+
|
|
|
+ ctx->last_sensor_update = now;
|
|
|
}
|
|
|
}
|
|
|
SDL_free(mapping);
|
|
@@ -1332,6 +1669,260 @@ void DestroyGamepadDisplay(GamepadDisplay *ctx)
|
|
|
SDL_free(ctx);
|
|
|
}
|
|
|
|
|
|
+void RenderSensorTimingInfo(GyroDisplay *ctx, GamepadDisplay *gamepad_display)
|
|
|
+{
|
|
|
+ /* Sensor timing section */
|
|
|
+ char text[128];
|
|
|
+ const float new_line_height = gamepad_display->button_height + 2.0f;
|
|
|
+ const float text_offset_x = ctx->area.x + ctx->area.w / 4.0f + 40.0f;
|
|
|
+ /* Anchor to bottom left of principle rect. */
|
|
|
+ float text_y_pos = ctx->area.y + ctx->area.h - new_line_height * 2;
|
|
|
+ /*
|
|
|
+ * Display rate of gyro as reported by the HID implementation.
|
|
|
+ * This could be based on a hardware time stamp (PS5), or it could be generated by the HID implementation.
|
|
|
+ * One should expect this to match the estimated rate below, assuming a wired connection.
|
|
|
+ */
|
|
|
+
|
|
|
+ SDL_strlcpy(text, "HID Sensor Time:", sizeof(text));
|
|
|
+ SDLTest_DrawString(ctx->renderer, text_offset_x - SDL_strlen(text) * FONT_CHARACTER_SIZE, text_y_pos, text);
|
|
|
+ if (ctx->reported_sensor_rate_hz > 0) {
|
|
|
+ /* Convert to micro seconds */
|
|
|
+ const int delta_time_us = (int)1e6 / ctx->reported_sensor_rate_hz;
|
|
|
+ SDL_snprintf(text, sizeof(text), "%d%ss %dhz", delta_time_us, MICRO_UTF8, ctx->reported_sensor_rate_hz);
|
|
|
+ } else {
|
|
|
+ SDL_snprintf(text, sizeof(text), "????%ss ???hz", MICRO_UTF8);
|
|
|
+ }
|
|
|
+ SDLTest_DrawString(ctx->renderer, text_offset_x + 2.0f, text_y_pos, text);
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Display the instrumentation's count of all sensor packets received over time.
|
|
|
+ * This may represent a more accurate polling rate for the IMU
|
|
|
+ * But only when using a wired connection.
|
|
|
+ * It does not necessarily reflect the rate at which the IMU is sampled.
|
|
|
+ */
|
|
|
+
|
|
|
+ text_y_pos += new_line_height;
|
|
|
+ SDL_strlcpy(text, "Est.Sensor Time:", sizeof(text));
|
|
|
+ SDLTest_DrawString(ctx->renderer, text_offset_x - SDL_strlen(text) * FONT_CHARACTER_SIZE, text_y_pos, text);
|
|
|
+ if (ctx->estimated_sensor_rate_hz > 0) {
|
|
|
+ /* Convert to micro seconds */
|
|
|
+ const int delta_time_us = (int)1e6 / ctx->estimated_sensor_rate_hz;
|
|
|
+ SDL_snprintf(text, sizeof(text), "%d%ss %dhz", delta_time_us, MICRO_UTF8, ctx->estimated_sensor_rate_hz);
|
|
|
+ } else {
|
|
|
+ SDL_snprintf(text, sizeof(text), "????%ss ???hz", MICRO_UTF8);
|
|
|
+ }
|
|
|
+ SDLTest_DrawString(ctx->renderer, text_offset_x + 2.0f, text_y_pos, text);
|
|
|
+}
|
|
|
+
|
|
|
+void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_display )
|
|
|
+{
|
|
|
+ char label_text[128];
|
|
|
+ float log_y = ctx->area.y + BUTTON_PADDING;
|
|
|
+ const float new_line_height = gamepad_display->button_height + 2.0f;
|
|
|
+ GamepadButton *start_calibration_button = GetGyroCalibrateButton(ctx);
|
|
|
+ bool bHasCachedDriftSolution = BHasCachedGyroDriftSolution(ctx);
|
|
|
+
|
|
|
+ /* Show the recalibration progress bar. */
|
|
|
+ float recalibrate_button_width = GetGamepadButtonLabelWidth(start_calibration_button) + 2 * BUTTON_PADDING;
|
|
|
+ SDL_FRect recalibrate_button_area;
|
|
|
+ recalibrate_button_area.x = ctx->area.x + ctx->area.w - recalibrate_button_width - BUTTON_PADDING;
|
|
|
+ recalibrate_button_area.y = log_y + FONT_CHARACTER_SIZE * 0.5f - gamepad_display->button_height * 0.5f;
|
|
|
+ recalibrate_button_area.w = GetGamepadButtonLabelWidth(start_calibration_button) + 2.0f * BUTTON_PADDING;
|
|
|
+ recalibrate_button_area.h = gamepad_display->button_height + BUTTON_PADDING * 2.0f;
|
|
|
+
|
|
|
+ if (!bHasCachedDriftSolution) {
|
|
|
+ SDL_snprintf(label_text, sizeof(label_text), "Progress: %3.0f%% ", ctx->drift_calibration_progress_frac * 100.0f);
|
|
|
+ } else {
|
|
|
+ SDL_strlcpy(label_text, "Calibrate Drift", sizeof(label_text));
|
|
|
+ }
|
|
|
+
|
|
|
+ SetGamepadButtonLabel(start_calibration_button, label_text);
|
|
|
+ SetGamepadButtonArea(start_calibration_button, &recalibrate_button_area);
|
|
|
+ RenderGamepadButton(start_calibration_button);
|
|
|
+
|
|
|
+ /* Above button */
|
|
|
+ SDL_strlcpy(label_text, "Gyro Orientation:", sizeof(label_text));
|
|
|
+ SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y - new_line_height, label_text);
|
|
|
+
|
|
|
+ if (!bHasCachedDriftSolution) {
|
|
|
+
|
|
|
+ float flNoiseFraction = SDL_clamp(SDL_sqrtf(ctx->accelerometer_noise_sq) / ACCELEROMETER_NOISE_THRESHOLD, 0.0f, 1.0f);
|
|
|
+ bool bTooMuchNoise = (flNoiseFraction == 1.0f);
|
|
|
+
|
|
|
+ float noise_bar_height = gamepad_display->button_height;
|
|
|
+ SDL_FRect noise_bar_rect;
|
|
|
+ noise_bar_rect.x = recalibrate_button_area.x;
|
|
|
+ noise_bar_rect.y = recalibrate_button_area.y + recalibrate_button_area.h + BUTTON_PADDING;
|
|
|
+ noise_bar_rect.w = recalibrate_button_area.w;
|
|
|
+ noise_bar_rect.h = noise_bar_height;
|
|
|
+
|
|
|
+ /* Adjust the noise bar rectangle based on the accelerometer noise value */
|
|
|
+
|
|
|
+ float noise_bar_fill_width = flNoiseFraction * noise_bar_rect.w; /* Scale the width based on the noise value */
|
|
|
+ SDL_FRect noise_bar_fill_rect;
|
|
|
+ noise_bar_fill_rect.x = noise_bar_rect.x + (noise_bar_rect.w - noise_bar_fill_width) * 0.5f;
|
|
|
+ noise_bar_fill_rect.y = noise_bar_rect.y;
|
|
|
+ noise_bar_fill_rect.w = noise_bar_fill_width;
|
|
|
+ noise_bar_fill_rect.h = noise_bar_height;
|
|
|
+
|
|
|
+ /* Set the color based on the noise value */
|
|
|
+ Uint8 red = (Uint8)(flNoiseFraction * 255.0f);
|
|
|
+ Uint8 green = (Uint8)((1.0f - flNoiseFraction) * 255.0f);
|
|
|
+ SDL_SetRenderDrawColor(ctx->renderer, red, green, 0, 255); /* red when high noise, green when low noise */
|
|
|
+ SDL_RenderFillRect(ctx->renderer, &noise_bar_fill_rect); /* draw the filled rectangle */
|
|
|
+
|
|
|
+ SDL_SetRenderDrawColor(ctx->renderer, 100, 100, 100, 255); /* gray box */
|
|
|
+ SDL_RenderRect(ctx->renderer, &noise_bar_rect); /* draw the outline rectangle */
|
|
|
+
|
|
|
+ /* Explicit warning message if we detect too much movement */
|
|
|
+ if (bTooMuchNoise) {
|
|
|
+ SDL_strlcpy(label_text, "Place GamePad Down!", sizeof(label_text));
|
|
|
+ SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, noise_bar_rect.y + noise_bar_rect.h + new_line_height, label_text);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Drift progress bar */
|
|
|
+ /* Demonstrate how far we are through the drift progress, and how it resets when there's "high noise", i.e if flNoiseFraction == 1.0f */
|
|
|
+ SDL_FRect progress_bar_rect;
|
|
|
+ progress_bar_rect.x = recalibrate_button_area.x + BUTTON_PADDING;
|
|
|
+ progress_bar_rect.y = recalibrate_button_area.y + recalibrate_button_area.h * 0.5f + BUTTON_PADDING * 0.5f;
|
|
|
+ progress_bar_rect.w = recalibrate_button_area.w - BUTTON_PADDING * 2.0f;
|
|
|
+ progress_bar_rect.h = BUTTON_PADDING * 0.5f;
|
|
|
+
|
|
|
+ /* Adjust the drift bar rectangle based on the drift calibration progress fraction */
|
|
|
+ float drift_bar_fill_width = bTooMuchNoise ? 1.0f : ctx->drift_calibration_progress_frac * progress_bar_rect.w;
|
|
|
+ SDL_FRect progress_bar_fill;
|
|
|
+ progress_bar_fill.x = progress_bar_rect.x;
|
|
|
+ progress_bar_fill.y = progress_bar_rect.y;
|
|
|
+ progress_bar_fill.w = drift_bar_fill_width;
|
|
|
+ progress_bar_fill.h = progress_bar_rect.h;
|
|
|
+
|
|
|
+ /* Set the color based on the drift calibration progress fraction */
|
|
|
+ SDL_SetRenderDrawColor(ctx->renderer, GYRO_COLOR_GREEN); /* red when too much noise, green when low noise*/
|
|
|
+
|
|
|
+ /* Now draw the bars with the filled, then empty rectangles */
|
|
|
+ SDL_RenderFillRect(ctx->renderer, &progress_bar_fill); /* draw the filled rectangle*/
|
|
|
+ SDL_SetRenderDrawColor(ctx->renderer, 100, 100, 100, 255); /* gray box*/
|
|
|
+ SDL_RenderRect(ctx->renderer, &progress_bar_rect); /* draw the outline rectangle*/
|
|
|
+
|
|
|
+ /* If there is too much movement, we are going to draw two diagonal red lines between the progress rect corners.*/
|
|
|
+ if (bTooMuchNoise) {
|
|
|
+ SDL_SetRenderDrawColor(ctx->renderer, GYRO_COLOR_RED); /* red */
|
|
|
+ SDL_RenderFillRect(ctx->renderer, &progress_bar_fill); /* draw the filled rectangle */
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+float RenderEulerReadout(GyroDisplay *ctx, GamepadDisplay *gamepad_display )
|
|
|
+{
|
|
|
+ /* Get the mater button's width and base our width off that */
|
|
|
+ GamepadButton *master_button = GetGyroCalibrateButton(ctx);
|
|
|
+ SDL_FRect gyro_calibrate_button_rect;
|
|
|
+ GetGamepadButtonArea(master_button, &gyro_calibrate_button_rect);
|
|
|
+
|
|
|
+ char text[128];
|
|
|
+ float log_y = gyro_calibrate_button_rect.y + gyro_calibrate_button_rect.h + BUTTON_PADDING;
|
|
|
+ const float new_line_height = gamepad_display->button_height + 2.0f;
|
|
|
+ float log_gyro_euler_text_x = gyro_calibrate_button_rect.x;
|
|
|
+
|
|
|
+ /* Pitch Readout */
|
|
|
+ SDL_snprintf(text, sizeof(text), "Pitch: %6.2f%s", ctx->euler_displacement_angles[0], DEGREE_UTF8);
|
|
|
+ SDLTest_DrawString(ctx->renderer, log_gyro_euler_text_x + 2.0f, log_y, text);
|
|
|
+
|
|
|
+ /* Yaw Readout */
|
|
|
+ log_y += new_line_height;
|
|
|
+ SDL_snprintf(text, sizeof(text), "Yaw: %6.2f%s", ctx->euler_displacement_angles[1], DEGREE_UTF8);
|
|
|
+ SDLTest_DrawString(ctx->renderer, log_gyro_euler_text_x + 2.0f, log_y, text);
|
|
|
+
|
|
|
+ /* Roll Readout */
|
|
|
+ log_y += new_line_height;
|
|
|
+ SDL_snprintf(text, sizeof(text), "Roll: %6.2f%s", ctx->euler_displacement_angles[2], DEGREE_UTF8);
|
|
|
+ SDLTest_DrawString(ctx->renderer, log_gyro_euler_text_x + 2.0f, log_y, text);
|
|
|
+
|
|
|
+ return log_y + new_line_height; /* Return the next y position for further rendering */
|
|
|
+}
|
|
|
+
|
|
|
+/* Draws the 3D cube, circles and accel arrow, positioning itself relative to the calibrate button. */
|
|
|
+void RenderGyroGizmo(GyroDisplay *ctx, SDL_Gamepad *gamepad, float top)
|
|
|
+{
|
|
|
+ /* Get the calibrate button's on-screen area: */
|
|
|
+ GamepadButton *btn = GetGyroCalibrateButton(ctx);
|
|
|
+ SDL_FRect btnArea;
|
|
|
+ GetGamepadButtonArea(btn, &btnArea);
|
|
|
+
|
|
|
+ float gizmoSize = btnArea.w;
|
|
|
+ /* Position it centered horizontally above the button with a small gap */
|
|
|
+ SDL_FRect gizmoRect;
|
|
|
+ gizmoRect.x = btnArea.x + (btnArea.w - gizmoSize) * 0.5f;
|
|
|
+ gizmoRect.y = top;
|
|
|
+ gizmoRect.w = gizmoSize;
|
|
|
+ gizmoRect.h = gizmoSize;
|
|
|
+
|
|
|
+ /* Draw the rotated cube */
|
|
|
+ DrawGyroDebugCube(ctx->renderer, &ctx->gyro_quaternion, &gizmoRect);
|
|
|
+
|
|
|
+ /* Overlay the XYZ circles */
|
|
|
+ DrawGyroDebugCircle(ctx->renderer, &ctx->gyro_quaternion, &gizmoRect);
|
|
|
+
|
|
|
+ /* If we have accel, draw that arrow too */
|
|
|
+ if (SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL)) {
|
|
|
+ float accel[3];
|
|
|
+ SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_ACCEL, accel, SDL_arraysize(accel));
|
|
|
+ DrawAccelerometerDebugArrow(ctx->renderer, &ctx->gyro_quaternion, accel, &gizmoRect);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Follow the size of the main button, but position it below the gizmo */
|
|
|
+ GamepadButton *reset_button = GetGyroResetButton(ctx);
|
|
|
+ if (reset_button) {
|
|
|
+ SDL_FRect reset_area;
|
|
|
+ GetGamepadButtonArea(reset_button, &reset_area);
|
|
|
+ /* Position the reset button below the gizmo */
|
|
|
+ reset_area.x = btnArea.x;
|
|
|
+ reset_area.y = gizmoRect.y + gizmoRect.h + BUTTON_PADDING * 0.5f;
|
|
|
+ reset_area.w = btnArea.w;
|
|
|
+ reset_area.h = btnArea.h;
|
|
|
+ SetGamepadButtonArea(reset_button, &reset_area);
|
|
|
+ RenderGamepadButton(reset_button);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RenderGyroDisplay(GyroDisplay *ctx, GamepadDisplay *gamepadElements, SDL_Gamepad *gamepad)
|
|
|
+{
|
|
|
+ if (!ctx)
|
|
|
+ return;
|
|
|
+
|
|
|
+ bool bHasAccelerometer = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL);
|
|
|
+ bool bHasGyroscope = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO);
|
|
|
+ bool bHasIMU = bHasAccelerometer || bHasGyroscope;
|
|
|
+ if (!bHasIMU)
|
|
|
+ return;
|
|
|
+
|
|
|
+ Uint8 r, g, b, a;
|
|
|
+ SDL_GetRenderDrawColor(ctx->renderer, &r, &g, &b, &a);
|
|
|
+
|
|
|
+ RenderSensorTimingInfo(ctx, gamepadElements);
|
|
|
+
|
|
|
+ RenderGyroDriftCalibrationButton(ctx, gamepadElements);
|
|
|
+
|
|
|
+ bool bHasCachedDriftSolution = BHasCachedGyroDriftSolution(ctx);
|
|
|
+ if (bHasCachedDriftSolution) {
|
|
|
+ float bottom = RenderEulerReadout(ctx, gamepadElements);
|
|
|
+ RenderGyroGizmo(ctx, gamepad, bottom);
|
|
|
+
|
|
|
+ }
|
|
|
+ SDL_SetRenderDrawColor(ctx->renderer, r, g, b, a);
|
|
|
+}
|
|
|
+
|
|
|
+void DestroyGyroDisplay(GyroDisplay *ctx)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ DestroyGamepadButton(ctx->reset_gyro_button);
|
|
|
+ DestroyGamepadButton(ctx->calibrate_gyro_button);
|
|
|
+ SDL_free(ctx);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
struct GamepadTypeDisplay
|
|
|
{
|
|
|
SDL_Renderer *renderer;
|
|
@@ -1965,13 +2556,25 @@ GamepadButton *CreateGamepadButton(SDL_Renderer *renderer, const char *label)
|
|
|
ctx->background = CreateTexture(renderer, gamepad_button_background_bmp, gamepad_button_background_bmp_len);
|
|
|
SDL_GetTextureSize(ctx->background, &ctx->background_width, &ctx->background_height);
|
|
|
|
|
|
- ctx->label = SDL_strdup(label);
|
|
|
- ctx->label_width = (float)(FONT_CHARACTER_SIZE * SDL_strlen(label));
|
|
|
- ctx->label_height = (float)FONT_CHARACTER_SIZE;
|
|
|
+ SetGamepadButtonLabel(ctx, label);
|
|
|
}
|
|
|
return ctx;
|
|
|
}
|
|
|
|
|
|
+void SetGamepadButtonLabel(GamepadButton *ctx, const char *label)
|
|
|
+{
|
|
|
+ if (!ctx) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ctx->label) {
|
|
|
+ SDL_free(ctx->label);
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx->label = SDL_strdup(label);
|
|
|
+ ctx->label_width = (float)(FONT_CHARACTER_SIZE * SDL_strlen(label));
|
|
|
+ ctx->label_height = (float)FONT_CHARACTER_SIZE;
|
|
|
+}
|
|
|
void SetGamepadButtonArea(GamepadButton *ctx, const SDL_FRect *area)
|
|
|
{
|
|
|
if (!ctx) {
|