/* * Example program for the Allegro library, by Jason Wilkins. * * Euler angles are convenient for storing and creating 3D orientations. * However, this program demonstrates that they are not good when * interpolating between two different orientations. The problem is * solved by using Allegro's quaternion operations. * * In this program, two cubes are rotated between random orientations. * Notice that although they have the same beginning and ending * orientations, they do not follow the same path between orientations. * * One cube is being rotated by directly incrementing or decrementing * the Euler angles from the starting point to the ending point. * This is an intuitive notion, but it is incorrect because it does not * cause the object to turn around a single unchanging axis of rotation. * The axis of rotation wobbles resulting in the object spinning in * strange ways. The object will eventually end up in the orientation * that the user intended, but it gets there in a way that is unattractive. * Imagine if this method was used to update the position of a camera in a * game! Sometimes it would swing wildly and disorient the player. * * The other cube is animated using quaternions. This results in a much * more pleasing animation because the cube turns around a single axis * of rotation. */ #include #include #include /* the number of steps to get from the starting to the ending orientation */ #define NUM_STEPS 200 /* this structure holds an orientation expressed as Euler angles. Each number * represents a rotation about the x, y, and z axis. In the case of Allegro * there are 256 degrees to a circle. Yaw, pitch, and roll correspond to * x, y, and z */ typedef struct EULER { float x, y, z; } EULER; /* matrix to transform world coordinates into normalized eye coordinates */ MATRIX_f camera; MATRIX_f rotation; /* the parts of the screen that display the demo boxes */ BITMAP *euler_screen; BITMAP *quat_screen; /* these are backbuffers, drawing is done here before updating the screen */ BITMAP *euler_buffer; BITMAP *quat_buffer; /* In these identifiers, 'from' refers to the starting orientation, 'to' * refers to the ending orientation and 'in' refers to the interpolated * orientation. 'q' refers to quaternion, 'e' refers to Euler */ MATRIX_f q_from_matrix; MATRIX_f q_to_matrix; MATRIX_f q_in_matrix; MATRIX_f e_from_matrix; MATRIX_f e_to_matrix; MATRIX_f e_in_matrix; QUAT q_to; QUAT q_in; QUAT q_from; EULER e_from; EULER e_to; EULER e_in; /* Here is defined a 2x2x2 cube centered about the origin, and * an arrow pointing straight up. They are wireframe objects * so only the points and edges are specified. * * It should be noted that the world coordinate system in this * program is oriented like it is in most math books. X and Y * are oriented like a floor and Z refers to the height above * that floor. (Mathematically, this is known as right-handed * coordinate system.) * * N - North * S - South * W - West * E - East * U - Up * D - Down */ float box_points[8][3] = { /* X, Y, Z */ { -1.0, -1.0, -1.0 }, /* NWD */ { -1.0, -1.0, 1.0 }, /* NWU */ { -1.0, 1.0, -1.0 }, /* NED */ { -1.0, 1.0, 1.0 }, /* NEU */ { 1.0, -1.0, -1.0 }, /* SWD */ { 1.0, -1.0, 1.0 }, /* SWU */ { 1.0, 1.0, -1.0 }, /* SED */ { 1.0, 1.0, 1.0 }, /* SEU */ }; int box_edges[12][2] = { /* from, to */ { 0, 2 }, /* bottom */ { 2, 6 }, { 6, 4 }, { 4, 0 }, { 1, 3 }, /* top */ { 3, 7 }, { 7, 5 }, { 5, 1 }, { 0, 1 }, /* sides */ { 2, 3 }, { 4, 5 }, { 6, 7 } }; float arrow_points[4][3] = { /* X, Y, Z */ { 0.0, 0.0, 0.0 }, /* tail of the arrow, at the origin */ { 0.0, 0.0, 2.0 }, /* tip of the arrow head */ { 0.0, 0.25, 1.5 }, /* eastern part of the head */ { 0.0, -0.25, 1.5 } /* western part of the head */ }; int arrow_edges[3][2] = { /* from, to */ { 0, 1 }, { 1, 2 }, { 1, 3 } }; /* Each demo box has associated with it two paths (stored as wireframe * objects). These are used to store a history of the orientation of their * interpolated axis. These sets of points are used to draw ribbons that * show how an object rotated from one orientation to another. */ float e_path_points_1[NUM_STEPS+1][3]; float e_path_points_2[NUM_STEPS+1][3]; float q_path_points_1[NUM_STEPS+1][3]; float q_path_points_2[NUM_STEPS+1][3]; /* these arrays are shared by both ribbons */ float tmp_points[NUM_STEPS+1][3]; int path_edges[NUM_STEPS][2]; /* draw an object defined as a set of points and edges */ void render_wireframe_object(MATRIX_f *m, BITMAP *b, float p[][3], float t[][3], int e[][2], int np, int ne, int c) { int index, from, to; /* transform the points and store them in a buffer */ for (index=0; indexx-from->x) * t; out->x = from->x+delta; delta = (to->y-from->y) * t; out->y = from->y+delta; delta = (to->z-from->z) * t; out->z = from->z+delta; } int main(void) { int index; if (allegro_init() != 0) return 1; install_keyboard(); if (set_gfx_mode(GFX_AUTODETECT, 640, 480, 0, 0) != 0) { if (set_gfx_mode(GFX_SAFE, 640, 480, 0, 0) != 0) { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0); allegro_message("Unable to set any graphic mode\n%s\n", allegro_error); return 1; } } set_palette(desktop_palette); clear_to_color(screen, palette_color[0]); /* Each back-buffer is one quarter the size of the screen */ euler_buffer = create_bitmap(320, 240); quat_buffer = create_bitmap(320, 240); if ((!euler_buffer) || (!quat_buffer)) { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0); allegro_message("Error creating bitmaps\n"); return 1; } set_palette(desktop_palette); /* setup the viewport for rendering into the back-buffers */ set_projection_viewport(0, 0, 320, 240); /* print out something helpful for the user */ textout_ex(screen, font, "SPACE - next interpolation", 184, 24, palette_color[15], -1); textout_ex(screen, font, " R - repeat last interpolation", 184, 40, palette_color[15], -1); textout_ex(screen, font, " ESC - quit", 184, 56, palette_color[15], -1); textout_ex(screen, font, "Interpolating Euler Angles", 56, 110, palette_color[15], -1); textout_ex(screen, font, "Interpolating Quaternions", 380, 110, palette_color[15], -1); textout_ex(screen, font, "Incorrect!", 120, 360, palette_color[15], -1); textout_ex(screen, font, "Correct!", 448, 360, palette_color[15], -1); /* initialize the path edges. This structure is used by both the Euler * path and the quaternion path. It connects all the points end to end */ for (index=0; index<(NUM_STEPS-1); index++) { path_edges[index][0] = index; path_edges[index][1] = index + 1; } /* initialize the first destination orientation */ srand(time(NULL)); e_to.x = (float)(AL_RAND() % 256); e_to.y = (float)(AL_RAND() % 256); e_to.z = (float)(AL_RAND() % 256); /* the camera is backed away from the origin and turned to face it */ get_camera_matrix_f(&camera, 5, 0, 0, -1, 0, 0, 0, 0, 1, 46, 1.33f); /* this is a 'for'ever loop */ for (;;) { float t; clear_keybuf(); for (index=0; index<(NUM_STEPS+1); index++) { if (keypressed()) break; t = index * (1.0 / NUM_STEPS); /* the first part shows how to animate the cube incorrectly * using Euler angles */ /* create the matrix for the starting orientation */ get_rotation_matrix_f(&rotation, e_from.x, e_from.y, e_from.z); matrix_mul_f(&rotation, &camera, &e_from_matrix); /* create the matrix for the ending orientation */ get_rotation_matrix_f(&rotation, e_to.x, e_to.y, e_to.z); matrix_mul_f(&rotation, &camera, &e_to_matrix); /* use the incorrect method to interpolate between them */ euler_interpolate(&e_from, &e_to, t, &e_in); get_rotation_matrix_f(&rotation, e_in.x, e_in.y, e_in.z); matrix_mul_f(&rotation, &camera, &e_in_matrix); /* update the lines that make up the Euler orientation path */ apply_matrix_f(&rotation, 0, 0, 1.5, &(e_path_points_1[index][0]), &(e_path_points_1[index][1]), &(e_path_points_1[index][2])); apply_matrix_f(&rotation, 0, 0, 2.0, &(e_path_points_2[index][0]), &(e_path_points_2[index][1]), &(e_path_points_2[index][2])); /* render the results to the Euler sub-bitmap */ clear_to_color(euler_buffer, palette_color[0]); render_demo_box(euler_buffer, &e_from_matrix, &e_in_matrix, &e_to_matrix, palette_color[15], palette_color[1], palette_color[4]); render_wireframe_object(&camera, euler_buffer, e_path_points_1, tmp_points, path_edges, index+1, index, palette_color[5]); render_wireframe_object(&camera, euler_buffer, e_path_points_2, tmp_points, path_edges, index+1, index, palette_color[5]); /* here is how to animate the cube correctly using quaternions */ /* create a matrix for the starting orientation. This time * we create it using quaternions. This is to demonstrate * that the quaternion gotten with get_rotation_quat will * generate the same matrix as that gotten by get_rotation_matrix */ get_rotation_quat(&q_from, e_from.x, e_from.y, e_from.z); quat_to_matrix(&q_from, &rotation); matrix_mul_f(&rotation, &camera, &q_from_matrix); /* this is the same as above, but for the ending orientation */ get_rotation_quat(&q_to, e_to.x, e_to.y, e_to.z); quat_to_matrix(&q_to, &rotation); matrix_mul_f(&rotation, &camera, &q_to_matrix); /* quat_interpolate is the proper way to interpolate between two * orientations. */ quat_interpolate(&q_from, &q_to, t, &q_in); quat_to_matrix(&q_in, &rotation); matrix_mul_f(&rotation, &camera, &q_in_matrix); /* update the lines that make up the quaternion orientation path */ apply_matrix_f(&rotation, 0, 0, 1.5, &(q_path_points_1[index][0]), &(q_path_points_1[index][1]), &(q_path_points_1[index][2])); apply_matrix_f(&rotation, 0, 0, 2.0, &(q_path_points_2[index][0]), &(q_path_points_2[index][1]), &(q_path_points_2[index][2])); /* render the results to the quaternion sub-bitmap */ clear_to_color(quat_buffer, palette_color[0]); render_demo_box(quat_buffer, &q_from_matrix, &q_in_matrix, &q_to_matrix, palette_color[15], palette_color[1], palette_color[4]); render_wireframe_object(&camera, quat_buffer, q_path_points_1, tmp_points, path_edges, index+1, index, palette_color[5]); render_wireframe_object(&camera, quat_buffer, q_path_points_2, tmp_points, path_edges, index+1, index, palette_color[5]); acquire_bitmap(screen); blit(euler_buffer, screen, 0, 0, 0, 120, 320, 240); blit(quat_buffer, screen, 0, 0, 320, 120, 320, 240); release_bitmap(screen); rest (1); } /* handle user input */ for (;;) { int input = readkey() >> 8; if (input == KEY_R) { /* skip updating the EULER angles so that the last interpolation * will repeat */ break; } else if (input == KEY_SPACE) { /* make the last ending orientation the starting orientation and * generate a random new ending orientation */ e_from = e_to; e_to.x = (float)(AL_RAND() % 256); e_to.y = (float)(AL_RAND() % 256); e_to.z = (float)(AL_RAND() % 256); break; } else if (input == KEY_ESC) { /* quit the program */ destroy_bitmap(euler_buffer); destroy_bitmap(quat_buffer); return 0; } } } } END_OF_MAIN()