Skip to content

Commit e960bf6

Browse files
authored
Gyro instrumentation for test controller (#13287)
This adds several minor changes to the gyro instruments. * The HID Sensor Time display is now throttled to 10hz. * Calibration for the gyro is now time based, not sample count based. Different polling rates will have drift calibrated over the same space of time. * Pitch/Yaw/Roll readout: Yaw is prioritized, and then pitch, and then roll. This gives a more human-readable pitch/yaw/roll display, closely matching game engines. * Pitch/Yaw/Roll text is colorized to match the axes in the 3D gizmo. * Added set of axes to the 3D gizmo to show the "Left Hand Space" positive axis directions.
1 parent 89eef1b commit e960bf6

File tree

2 files changed

+121
-69
lines changed

2 files changed

+121
-69
lines changed

test/gamepadutils.c

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ static SDL_FPoint ProjectVec3ToRect(const Vector3 *v, const SDL_FRect *rect)
101101
float fovScaleX = fovScaleY * aspect;
102102

103103
float relZ = cameraZ - v->z;
104-
if (relZ < 0.01f)
104+
if (relZ < 0.01f) {
105105
relZ = 0.01f; /* Prevent division by 0 or negative depth */
106+
}
106107

107108
float ndc_x = (v->x / relZ) / fovScaleX;
108109
float ndc_y = (v->y / relZ) / fovScaleY;
@@ -207,6 +208,39 @@ void DrawGyroDebugCircle(SDL_Renderer *renderer, const Quaternion *orientation,
207208
SDL_SetRenderDrawColor(renderer, r, g, b, a);
208209
}
209210

211+
212+
void DrawGyroDebugAxes(SDL_Renderer *renderer, const Quaternion *orientation, const SDL_FRect *bounds)
213+
{
214+
/* Store current color */
215+
Uint8 r, g, b, a;
216+
SDL_GetRenderDrawColor(renderer, &r, &g, &b, &a);
217+
218+
Vector3 origin = { 0.0f, 0.0f, 0.0f };
219+
220+
Vector3 right = { 1.0f, 0.0f, 0.0f };
221+
Vector3 up = { 0.0f, 1.0f, 0.0f };
222+
Vector3 back = { 0.0f, 0.0f, 1.0f };
223+
224+
Vector3 world_right = RotateVectorByQuaternion(&right, orientation);
225+
Vector3 world_up = RotateVectorByQuaternion(&up, orientation);
226+
Vector3 world_back = RotateVectorByQuaternion(&back, orientation);
227+
228+
SDL_FPoint origin_screen = ProjectVec3ToRect(&origin, bounds);
229+
SDL_FPoint right_screen = ProjectVec3ToRect(&world_right, bounds);
230+
SDL_FPoint up_screen = ProjectVec3ToRect(&world_up, bounds);
231+
SDL_FPoint back_screen = ProjectVec3ToRect(&world_back, bounds);
232+
233+
SDL_SetRenderDrawColor(renderer, GYRO_COLOR_RED);
234+
SDL_RenderLine(renderer, origin_screen.x, origin_screen.y, right_screen.x, right_screen.y);
235+
SDL_SetRenderDrawColor(renderer, GYRO_COLOR_GREEN);
236+
SDL_RenderLine(renderer, origin_screen.x, origin_screen.y, up_screen.x, up_screen.y);
237+
SDL_SetRenderDrawColor(renderer, GYRO_COLOR_BLUE);
238+
SDL_RenderLine(renderer, origin_screen.x, origin_screen.y, back_screen.x, back_screen.y);
239+
240+
/* Restore current color */
241+
SDL_SetRenderDrawColor(renderer, r, g, b, a);
242+
}
243+
210244
void DrawAccelerometerDebugArrow(SDL_Renderer *renderer, const Quaternion *gyro_quaternion, const float *accel_data, const SDL_FRect *bounds)
211245
{
212246
/* Store current color */
@@ -990,6 +1024,8 @@ struct GyroDisplay
9901024
/* This part displays extra info from the IMUstate in order to figure out actual polling rates. */
9911025
float gyro_drift_solution[3];
9921026
int reported_sensor_rate_hz; /*hz - comes from HIDsdl implementation. Could be fixed, platform time, or true sensor time*/
1027+
Uint64 next_reported_sensor_time; /* SDL ticks used to throttle the display */
1028+
9931029
int estimated_sensor_rate_hz; /*hz - our estimation of the actual polling rate by observing packets received*/
9941030
float euler_displacement_angles[3]; /* pitch, yaw, roll */
9951031
Quaternion gyro_quaternion; /* Rotation since startup/reset, comprised of each gyro speed packet times sensor delta time. */
@@ -1009,7 +1045,8 @@ GyroDisplay *CreateGyroDisplay(SDL_Renderer *renderer)
10091045
SDL_zeroa(ctx->gyro_drift_solution);
10101046
Quaternion quat_identity = { 0.0f, 0.0f, 0.0f, 1.0f };
10111047
ctx->gyro_quaternion = quat_identity;
1012-
1048+
ctx->reported_sensor_rate_hz = 0;
1049+
ctx->next_reported_sensor_time = 0;
10131050
ctx->reset_gyro_button = CreateGamepadButton(renderer, "Reset View");
10141051
ctx->calibrate_gyro_button = CreateGamepadButton(renderer, "Recalibrate Drift");
10151052
}
@@ -1024,7 +1061,6 @@ void SetGyroDisplayArea(GyroDisplay *ctx, const SDL_FRect *area)
10241061
}
10251062

10261063
SDL_copyp(&ctx->area, area);
1027-
10281064
/* Place the reset button to the bottom right of the gyro display area.*/
10291065
SDL_FRect reset_button_area;
10301066
reset_button_area.w = SDL_max(MINIMUM_BUTTON_WIDTH, GetGamepadButtonLabelWidth(ctx->reset_gyro_button) + 2 * BUTTON_PADDING);
@@ -1340,12 +1376,17 @@ void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, fl
13401376
return;
13411377
}
13421378

1343-
SDL_memcpy(ctx->gyro_drift_solution, gyro_drift_solution, sizeof(ctx->gyro_drift_solution));
1344-
ctx->estimated_sensor_rate_hz = estimated_sensor_rate_hz;
1345-
1346-
if (reported_senor_rate_hz != 0)
1347-
ctx->reported_sensor_rate_hz = reported_senor_rate_hz;
1379+
const int SENSOR_UPDATE_INTERVAL_MS = 100;
1380+
Uint64 now = SDL_GetTicks();
1381+
if (now > ctx->next_reported_sensor_time) {
1382+
ctx->estimated_sensor_rate_hz = estimated_sensor_rate_hz;
1383+
if (reported_senor_rate_hz != 0) {
1384+
ctx->reported_sensor_rate_hz = reported_senor_rate_hz;
1385+
}
1386+
ctx->next_reported_sensor_time = now + SENSOR_UPDATE_INTERVAL_MS;
1387+
}
13481388

1389+
SDL_memcpy(ctx->gyro_drift_solution, gyro_drift_solution, sizeof(ctx->gyro_drift_solution));
13491390
SDL_memcpy(ctx->euler_displacement_angles, euler_displacement_angles, sizeof(ctx->euler_displacement_angles));
13501391
ctx->gyro_quaternion = *gyro_quaternion;
13511392
ctx->drift_calibration_progress_frac = drift_calibration_progress_frac;
@@ -1637,7 +1678,7 @@ void RenderGamepadDisplay(GamepadDisplay *ctx, SDL_Gamepad *gamepad)
16371678
SDLTest_DrawString(ctx->renderer, x + center - SDL_strlen(text) * FONT_CHARACTER_SIZE, y, text);
16381679
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);
16391680
SDLTest_DrawString(ctx->renderer, x + center + 2.0f, y, text);
1640-
1681+
16411682

16421683
/* Display the testcontroller tool's evaluation of drift. This is also useful to get an average rate of turn in calibrated turntable tests. */
16431684
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 )
@@ -1648,10 +1689,7 @@ void RenderGamepadDisplay(GamepadDisplay *ctx, SDL_Gamepad *gamepad)
16481689
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);
16491690
SDLTest_DrawString(ctx->renderer, x + center + 2.0f, y, text);
16501691
}
1651-
16521692
}
1653-
1654-
16551693
}
16561694
}
16571695
SDL_free(mapping);
@@ -1797,7 +1835,6 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_
17971835

17981836
/* Set the color based on the drift calibration progress fraction */
17991837
SDL_SetRenderDrawColor(ctx->renderer, GYRO_COLOR_GREEN); /* red when too much noise, green when low noise*/
1800-
18011838
/* Now draw the bars with the filled, then empty rectangles */
18021839
SDL_RenderFillRect(ctx->renderer, &progress_bar_fill); /* draw the filled rectangle*/
18031840
SDL_SetRenderDrawColor(ctx->renderer, 100, 100, 100, 255); /* gray box*/
@@ -1823,20 +1860,26 @@ float RenderEulerReadout(GyroDisplay *ctx, GamepadDisplay *gamepad_display )
18231860
const float new_line_height = gamepad_display->button_height + 2.0f;
18241861
float log_gyro_euler_text_x = gyro_calibrate_button_rect.x;
18251862

1863+
Uint8 r, g, b, a;
1864+
SDL_GetRenderDrawColor(ctx->renderer, &r, &g, &b, &a);
18261865
/* Pitch Readout */
1866+
SDL_SetRenderDrawColor(ctx->renderer, GYRO_COLOR_RED);
18271867
SDL_snprintf(text, sizeof(text), "Pitch: %6.2f%s", ctx->euler_displacement_angles[0], DEGREE_UTF8);
18281868
SDLTest_DrawString(ctx->renderer, log_gyro_euler_text_x + 2.0f, log_y, text);
18291869

18301870
/* Yaw Readout */
1871+
SDL_SetRenderDrawColor(ctx->renderer, GYRO_COLOR_GREEN);
18311872
log_y += new_line_height;
18321873
SDL_snprintf(text, sizeof(text), " Yaw: %6.2f%s", ctx->euler_displacement_angles[1], DEGREE_UTF8);
18331874
SDLTest_DrawString(ctx->renderer, log_gyro_euler_text_x + 2.0f, log_y, text);
18341875

18351876
/* Roll Readout */
1877+
SDL_SetRenderDrawColor(ctx->renderer, GYRO_COLOR_BLUE);
18361878
log_y += new_line_height;
18371879
SDL_snprintf(text, sizeof(text), " Roll: %6.2f%s", ctx->euler_displacement_angles[2], DEGREE_UTF8);
18381880
SDLTest_DrawString(ctx->renderer, log_gyro_euler_text_x + 2.0f, log_y, text);
18391881

1882+
SDL_SetRenderDrawColor(ctx->renderer, r, g, b, a);
18401883
return log_y + new_line_height; /* Return the next y position for further rendering */
18411884
}
18421885

@@ -1859,6 +1902,9 @@ void RenderGyroGizmo(GyroDisplay *ctx, SDL_Gamepad *gamepad, float top)
18591902
/* Draw the rotated cube */
18601903
DrawGyroDebugCube(ctx->renderer, &ctx->gyro_quaternion, &gizmoRect);
18611904

1905+
/* Draw positive axes */
1906+
DrawGyroDebugAxes(ctx->renderer, &ctx->gyro_quaternion, &gizmoRect);
1907+
18621908
/* Overlay the XYZ circles */
18631909
DrawGyroDebugCircle(ctx->renderer, &ctx->gyro_quaternion, &gizmoRect);
18641910

@@ -1906,7 +1952,6 @@ void RenderGyroDisplay(GyroDisplay *ctx, GamepadDisplay *gamepadElements, SDL_Ga
19061952
if (bHasCachedDriftSolution) {
19071953
float bottom = RenderEulerReadout(ctx, gamepadElements);
19081954
RenderGyroGizmo(ctx, gamepad, bottom);
1909-
19101955
}
19111956
SDL_SetRenderDrawColor(ctx->renderer, r, g, b, a);
19121957
}

test/testcontroller.c

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -53,62 +53,59 @@ struct Quaternion
5353

5454
static Quaternion quat_identity = { 0.0f, 0.0f, 0.0f, 1.0f };
5555

56-
Quaternion QuaternionFromEuler(float roll, float pitch, float yaw)
56+
Quaternion QuaternionFromEuler(float pitch, float yaw, float roll)
5757
{
58-
Quaternion q;
58+
float cx = SDL_cosf(pitch * 0.5f);
59+
float sx = SDL_sinf(pitch * 0.5f);
5960
float cy = SDL_cosf(yaw * 0.5f);
6061
float sy = SDL_sinf(yaw * 0.5f);
61-
float cp = SDL_cosf(pitch * 0.5f);
62-
float sp = SDL_sinf(pitch * 0.5f);
63-
float cr = SDL_cosf(roll * 0.5f);
64-
float sr = SDL_sinf(roll * 0.5f);
62+
float cz = SDL_cosf(roll * 0.5f);
63+
float sz = SDL_sinf(roll * 0.5f);
6564

66-
q.w = cr * cp * cy + sr * sp * sy;
67-
q.x = sr * cp * cy - cr * sp * sy;
68-
q.y = cr * sp * cy + sr * cp * sy;
69-
q.z = cr * cp * sy - sr * sp * cy;
65+
Quaternion q;
66+
q.w = cx * cy * cz + sx * sy * sz;
67+
q.x = sx * cy * cz - cx * sy * sz;
68+
q.y = cx * sy * cz + sx * cy * sz;
69+
q.z = cx * cy * sz - sx * sy * cz;
7070

7171
return q;
7272
}
7373

74-
static void EulerFromQuaternion(Quaternion q, float *roll, float *pitch, float *yaw)
75-
{
76-
float sinr_cosp = 2.0f * (q.w * q.x + q.y * q.z);
77-
float cosr_cosp = 1.0f - 2.0f * (q.x * q.x + q.y * q.y);
78-
float roll_rad = SDL_atan2f(sinr_cosp, cosr_cosp);
74+
#define RAD_TO_DEG (180.0f / SDL_PI_F)
7975

80-
float sinp = 2.0f * (q.w * q.y - q.z * q.x);
81-
float pitch_rad;
82-
if (SDL_fabsf(sinp) >= 1.0f) {
83-
pitch_rad = SDL_copysignf(SDL_PI_F / 2.0f, sinp);
84-
} else {
85-
pitch_rad = SDL_asinf(sinp);
86-
}
76+
/* Decomposes quaternion into Yaw (Y), Pitch (X), Roll (Z) using Y-X-Z order in a left-handed system */
77+
void QuaternionToYXZ(Quaternion q, float *pitch, float *yaw, float *roll)
78+
{
79+
/* Precalculate repeated expressions */
80+
float qxx = q.x * q.x;
81+
float qyy = q.y * q.y;
82+
float qzz = q.z * q.z;
8783

88-
float siny_cosp = 2.0f * (q.w * q.z + q.x * q.y);
89-
float cosy_cosp = 1.0f - 2.0f * (q.y * q.y + q.z * q.z);
90-
float yaw_rad = SDL_atan2f(siny_cosp, cosy_cosp);
84+
float qxy = q.x * q.y;
85+
float qxz = q.x * q.z;
86+
float qyz = q.y * q.z;
87+
float qwx = q.w * q.x;
88+
float qwy = q.w * q.y;
89+
float qwz = q.w * q.z;
9190

92-
if (roll)
93-
*roll = roll_rad;
94-
if (pitch)
95-
*pitch = pitch_rad;
96-
if (yaw)
97-
*yaw = yaw_rad;
98-
}
91+
/* Yaw (around Y) */
92+
if (yaw) {
93+
*yaw = SDL_atan2f(2.0f * (qwy + qxz), 1.0f - 2.0f * (qyy + qzz)) * RAD_TO_DEG;
94+
}
9995

100-
static void EulerDegreesFromQuaternion(Quaternion q, float *pitch, float *yaw, float *roll)
101-
{
102-
float pitch_rad, yaw_rad, roll_rad;
103-
EulerFromQuaternion(q, &pitch_rad, &yaw_rad, &roll_rad);
96+
/* Pitch (around X) */
97+
float sinp = 2.0f * (qwx - qyz);
10498
if (pitch) {
105-
*pitch = pitch_rad * (180.0f / SDL_PI_F);
106-
}
107-
if (yaw) {
108-
*yaw = yaw_rad * (180.0f / SDL_PI_F);
99+
if (SDL_fabsf(sinp) >= 1.0f) {
100+
*pitch = SDL_copysignf(90.0f, sinp); /* Clamp to avoid domain error */
101+
} else {
102+
*pitch = SDL_asinf(sinp) * RAD_TO_DEG;
103+
}
109104
}
105+
106+
/* Roll (around Z) */
110107
if (roll) {
111-
*roll = roll_rad * (180.0f / SDL_PI_F);
108+
*roll = SDL_atan2f(2.0f * (qwz + qxy), 1.0f - 2.0f * (qxx + qzz)) * RAD_TO_DEG;
112109
}
113110
}
114111

@@ -1375,7 +1372,16 @@ static void HandleGamepadGyroEvent(SDL_Event *event)
13751372
SDL_memcpy(controller->imu_state->gyro_data, event->gsensor.data, sizeof(controller->imu_state->gyro_data));
13761373
}
13771374

1375+
/* Two strategies for evaluating polling rate - one based on a fixed packet count, and one using a fixed time window.
1376+
* Smaller values in either will give you a more responsive polling rate estimate, but this may fluctuate more.
1377+
* Larger values in either will give you a more stable average but they will require more time to evaluate.
1378+
* Generally, wired connections tend to give much more stable
1379+
*/
1380+
/* #define SDL_USE_FIXED_PACKET_COUNT_FOR_ESTIMATION */
13781381
#define SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_COUNT 2048
1382+
#define SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_TIME_NS (SDL_NS_PER_SECOND * 2)
1383+
1384+
13791385
static void EstimatePacketRate()
13801386
{
13811387
Uint64 now_ns = SDL_GetTicksNS();
@@ -1384,17 +1390,22 @@ static void EstimatePacketRate()
13841390
}
13851391

13861392
/* Require a significant sample size before averaging rate. */
1393+
#ifdef SDL_USE_FIXED_PACKET_COUNT_FOR_ESTIMATION
13871394
if (controller->imu_state->imu_packet_counter >= SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_COUNT) {
13881395
Uint64 deltatime_ns = now_ns - controller->imu_state->starting_time_stamp_ns;
1389-
controller->imu_state->imu_estimated_sensor_rate = (Uint16)((controller->imu_state->imu_packet_counter * 1000000000ULL) / deltatime_ns);
1396+
controller->imu_state->imu_estimated_sensor_rate = (Uint16)((controller->imu_state->imu_packet_counter * SDL_NS_PER_SECOND) / deltatime_ns);
1397+
controller->imu_state->imu_packet_counter = 0;
13901398
}
1391-
1392-
/* Flush sampled data after a brief period so that the imu_estimated_sensor_rate value can be read.*/
1393-
if (controller->imu_state->imu_packet_counter >= SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_COUNT * 2) {
1394-
controller->imu_state->starting_time_stamp_ns = now_ns;
1399+
#else
1400+
Uint64 deltatime_ns = now_ns - controller->imu_state->starting_time_stamp_ns;
1401+
if (deltatime_ns >= SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_TIME_NS) {
1402+
controller->imu_state->imu_estimated_sensor_rate = (Uint16)((controller->imu_state->imu_packet_counter * SDL_NS_PER_SECOND) / deltatime_ns);
13951403
controller->imu_state->imu_packet_counter = 0;
13961404
}
1397-
++controller->imu_state->imu_packet_counter;
1405+
#endif
1406+
else {
1407+
++controller->imu_state->imu_packet_counter;
1408+
}
13981409
}
13991410

14001411
static void UpdateGamepadOrientation( Uint64 delta_time_ns )
@@ -1409,13 +1420,11 @@ static void UpdateGamepadOrientation( Uint64 delta_time_ns )
14091420

14101421
static void HandleGamepadSensorEvent( SDL_Event* event )
14111422
{
1412-
if (!controller) {
1413-
return;
1414-
}
1423+
if (!controller)
1424+
return;
14151425

1416-
if (controller->id != event->gsensor.which) {
1426+
if (controller->id != event->gsensor.which)
14171427
return;
1418-
}
14191428

14201429
if (event->gsensor.sensor == SDL_SENSOR_GYRO) {
14211430
HandleGamepadGyroEvent(event);
@@ -1428,13 +1437,12 @@ static void HandleGamepadSensorEvent( SDL_Event* event )
14281437
accelerometer and gyro events are received before progressing.
14291438
*/
14301439
if ( controller->imu_state->accelerometer_packet_number == controller->imu_state->gyro_packet_number ) {
1431-
14321440
EstimatePacketRate();
14331441
Uint64 sensorTimeStampDelta_ns = event->gsensor.sensor_timestamp - controller->imu_state->last_sensor_time_stamp_ns ;
14341442
UpdateGamepadOrientation(sensorTimeStampDelta_ns);
14351443

14361444
float display_euler_angles[3];
1437-
EulerDegreesFromQuaternion(controller->imu_state->integrated_rotation, &display_euler_angles[0], &display_euler_angles[1], &display_euler_angles[2]);
1445+
QuaternionToYXZ(controller->imu_state->integrated_rotation, &display_euler_angles[0], &display_euler_angles[1], &display_euler_angles[2]);
14381446

14391447
float drift_calibration_progress_frac = controller->imu_state->gyro_drift_sample_count / (float)SDL_GAMEPAD_IMU_MIN_GYRO_DRIFT_SAMPLE_COUNT;
14401448
int reported_polling_rate_hz = sensorTimeStampDelta_ns > 0 ? (int)(SDL_NS_PER_SECOND / sensorTimeStampDelta_ns) : 0;
@@ -2073,7 +2081,6 @@ SDL_AppResult SDLCALL SDL_AppEvent(void *appstate, SDL_Event *event)
20732081
event->gsensor.data[1],
20742082
event->gsensor.data[2],
20752083
event->gsensor.sensor_timestamp);
2076-
20772084
#endif /* VERBOSE_SENSORS */
20782085
HandleGamepadSensorEvent(event);
20792086
break;

0 commit comments

Comments
 (0)