Code for 3D turret aiming

January 14, 2017 — Say you have a 3D space game, with NPC ships that have turrets on them, and the turrets can be oriented any which way on those ships. How do you make the turrets swivel around and aim smoothly and intelligently?

Note: The code in these examples is released under the GPL v2 license.  See:

Step 1: Understand your turret model.

In my case, my turret model is oriented such that it shoots down the positive X axis, and can rotate around the vertical Y axis and rotate around the horizontal Z axis. This is the same canonical orientation used by all the models in my game — the “unrotated state” has from the model’s point of view, down the X axis being straight ahead, positive Y as “up” and positive Z to the right.

Step 2. Understanding the turret’s state.

  • The turret has a location (x, y, z)
  • The turret has a “rest orientation” This is the orientation when the turret is in an unrotated state, but perhaps attached to some other object in a way that gives this state some arbitrary non-constant rotation that is different than the unrotated state the identity quaternion represents.
  • The turret has a “current orientation” This is the “rest orientation” plus some other arbitrary orientation accounting for where ever the turret might currently be pointed.
  • The turret cannot rotate infinitely fast. There is some limit to how much the turret can rotate in each of its two axes per unit time.

Step 3. What do we want to be able to do?

We’d like to be able to ask the turret to aim at something, and get a new orientation that obeys the constraints of a turret (not freely rotating, but only rotating on two axes) and allowing the turret to be attached to something.

So let’s say we’d like a C function something like this:

struct turret_params {
        float elevation_rate_limit;     /* how much can turret move in one step in radians */
        float azimuth_rate_limit;
};

/* union quat is a quaternion */

union quat *turret_aim(double target_x, double target_y, double target_z,       /* in: world coord position of target */
                        double turret_x, double turret_y, double turret_z,      /* in: world coord position of turret */
                        union quat *turret_rest_orientation,                    /* in: orientation of turret at rest */
                        union quat *current_turret_orientation,                 /* in: Current orientation of turret */
                        const struct turret_params *turret,                     /* in: turret params, can be NULL */
                        union quat *new_turret_orientation);                    /* out: new orientation of turret */

So how do we write such a function? Here’s one approach.

1. Transform the target into the turret’s coordinate space
2. Calculate the desired azimuth and elevation angles
3. Calculate the turret’s current azimuth and elevation
4. Calculate the delta between current and desired azimuth and elevation
5. clamp these deltas to the maximum angular rate the turret can move
6. Apply deltas to current elevation and azimuth
7. Convert new elevation and azimuth to the new orientation quaternion

So, going through these one by one:

Step 0. Declare some variables we’ll need:

        union vec3 yaw_axis = { { 0.0, 1.0, 0.0 } };
        union quat yaw, pitch, inverse_rest;
        union vec3 pitch_axis;
        union vec3 to_target, to_current_aim;
        float current_azimuth, azimuth;
        float current_elevation, elevation;
        float delta_elevation, delta_azimuth;
        float xzdist;

Step 1. Transform the target into the turret’s coordinate space
(because we want to make step 2 easier)

	/* Compute vector from turret to target */
	union vec3 to_target;
        to_target.v.x = target_x - turret_x;
        to_target.v.y = target_y - turret_y;
        to_target.v.z = target_z - turret_z;		
	/* Compute inverse rotation from turret rest orientation */
        quat_inverse(&inverse_rest, turret_rest_orientation);
	/* Convert target into turret's coordinate space */
	quat_rot_vec_self(&to_target, &inverse_rest);

Step 2. Calculate the desired azimuth and elevation angles:

        azimuth = atan2f(-to_target.v.z, to_target.v.x);
        xzdist = sqrtf(to_target.v.z * to_target.v.z + to_target.v.x * to_target.v.x);
        elevation = atan2f(to_target.v.y, xzdist);

At this point, you could skip to step 7, and the turret would snap instantly
to the correct orientation — but we want it to move smoothly.

Step 3. Calculate the turret’s current azimuth and elevation

        to_current_aim.v.x = 1.0; /* vector pointing down the x-axis */
        to_current_aim.v.y = 0.0;
        to_current_aim.v.z = 0.0;

	/* Rotate it into the current turret's orientation -- now the vector
	 * points in the same direction as the turret points in world coord
	 * system.
	 */
        quat_rot_vec_self(&to_current_aim, current_turret_orientation);

	/* Now convert into the turret's local coord system */
        quat_rot_vec_self(&to_current_aim, &inverse_rest);

	/* Now calculate current azimuth and elevation of the turret */
        current_azimuth = atan2f(-to_current_aim.v.z, to_current_aim.v.x);
        xzdist = sqrtf(to_current_aim.v.z * to_current_aim.v.z + to_current_aim.v.x * to_current_aim.v.x);
        current_elevation = atan2f(to_current_aim.v.y, xzdist);

Step 4. Calculate the delta between current and desired azimuth and elevation

        delta_elevation = elevation - current_elevation;
        delta_azimuth = azimuth - current_azimuth;

Step 5. Clamp these deltas to the maximum angular rate the turret can move in both axes.

        delta_elevation = elevation - current_elevation;
        delta_azimuth = azimuth - current_azimuth;
        if (delta_azimuth > turret->azimuth_rate_limit)
                delta_azimuth = turret->azimuth_rate_limit;
        else if (delta_azimuth < -turret->azimuth_rate_limit)
                delta_azimuth = -turret->azimuth_rate_limit;
        if (delta_elevation > turret->elevation_rate_limit)
                delta_elevation = turret->elevation_rate_limit;
        else if (delta_elevation < -turret->elevation_rate_limit)
                delta_elevation = -turret->elevation_rate_limit;

Step 6. Apply deltas to current elevation and azimuth

        azimuth = current_azimuth + delta_azimuth;
        elevation = current_elevation + delta_elevation;

Step 7. Convert new elevation and azimuth to the new orientation quaternion

	/* Recall yaw_axis is a vector pointing up the Y axis.  Rotate it
	 * into the turret's coordinate space, and build a yaw quaternion
	 * from this rotated yaw_axis and the azimuth
	 */
        quat_rot_vec_self(&yaw_axis, turret_rest_orientation);
        quat_init_axis(&yaw, yaw_axis.v.x, yaw_axis.v.y, yaw_axis.v.z, azimuth);
	/* Rotate the turret_rest_quaternion by the yaw quaternion */
        quat_mul(new_turret_orientation, &yaw, turret_rest_orientation);

	/* Set up a pitch axis -- around the Z axis */
        pitch_axis.v.x = 0.0;
        pitch_axis.v.y = 0.0;
        pitch_axis.v.z = 1.0;

	/* Rotate it into the turret's coordinate space (which has the yaw
	 * incorporated already now) and build a pitch quaternion out of the
	 * rotated pitch axis and elevation
	 */
        quat_rot_vec_self(&pitch_axis, new_turret_orientation);
        quat_init_axis(&pitch, pitch_axis.v.x, pitch_axis.v.y, pitch_axis.v.z, elevation);
	/* Rotate the turret orientation by the pitch quaternion */
        quat_mul(new_turret_orientation, &pitch, new_turret_orientation);

        return new_turret_orientation; /* and we're done. */

 

~ by scaryreasoner on January 15, 2017.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: