Implementing quaternions into Quake 3

By JackOfAllTrades

One thing I've noticed lately in every FPS game I've played is that gibs don't roll and and tumble properly. Can it really be that hard to do? Well, yes it is, if you use an Euler angle orientation system (like Q3 does). After fooling around a bit I quickly discovered that it was nigh on impossible to rotate an object about an arbitrary axis using Q3's angles system. Just try it: give an object angular velocities on more than one axis and you'll end up with a weird spin/wiggle effect that looks worse than not rotating the object at all. I'd heard of quaternions before and knew that they were said to be good for describing arbitrary rotations, but that's all I knew. After a bit of googling I was ready to try my hand at applying quaternions to Q3. Here is the result.

Start off by adding the following quaternion manipulation functions to q_math.c (these functions were adapted from Nick Bobic's quaternion article on Gamasutra). I know that modifying the q_* files is generally frowned upon but I think that in this case q_math.c is the logical place for these functions.

void AnglesToQuat (const vec3_t angles, vec4_t quat)
{
	vec3_t a;
	float cr, cp, cy, sr, sp, sy, cpcy, spsy;

	a[PITCH] = (M_PI/360.0) * angles[PITCH];
	a[YAW] = (M_PI/360.0) * angles[YAW];
	a[ROLL] = (M_PI/360.0) * angles[ROLL];

	cr = cos(a[ROLL]);
	cp = cos(a[PITCH]);
	cy = cos(a[YAW]);

	sr = sin(a[ROLL]);
	sp = sin(a[PITCH]);
	sy = sin(a[YAW]);

	cpcy = cp * cy;
	spsy = sp * sy;
	quat[0] = cr * cpcy + sr * spsy; // w
	quat[1] = sr * cpcy - cr * spsy; // x
	quat[2] = cr * sp * cy + sr * cp * sy; // y
	quat[3] = cr * cp * sy - sr * sp * cy; // z
}

void QuatToAxis(vec4_t q, vec3_t axis[3])
{
	float wx, wy, wz, xx, yy, yz, xy, xz, zz, x2, y2, z2;

	x2 = q[1] + q[1];
	y2 = q[2] + q[2];
	z2 = q[3] + q[3];
	xx = q[1] * x2;
	xy = q[1] * y2;
	xz = q[1] * z2;
	yy = q[2] * y2;
	yz = q[2] * z2;
	zz = q[3] * z2;
	wx = q[0] * x2;
	wy = q[0] * y2;
	wz = q[0] * z2;

	axis[0][0] = 1.0 - (yy + zz);
	axis[1][0] = xy - wz;
	axis[2][0] = xz + wy;

	axis[0][1] = xy + wz;
	axis[1][1] = 1.0 - (xx + zz);
	axis[2][1] = yz - wx;

	axis[0][2] = xz - wy;
	axis[1][2] = yz + wx;
	axis[2][2] = 1.0 - (xx + yy);
}

void QuatMul(const vec4_t q1, const vec4_t q2, vec4_t res)
{
	float A, B, C, D, E, F, G, H;

	A = (q1[0] + q1[1])*(q2[0] + q2[1]);
	B = (q1[3] - q1[2])*(q2[2] - q2[3]);
	C = (q1[0] - q1[1])*(q2[2] + q2[3]);
	D = (q1[2] + q1[3])*(q2[0] - q2[1]);
	E = (q1[1] + q1[3])*(q2[1] + q2[2]);
	F = (q1[1] - q1[3])*(q2[1] - q2[2]);
	G = (q1[0] + q1[2])*(q2[0] - q2[3]);
	H = (q1[0] - q1[2])*(q2[0] + q2[3]);

	res[0] = B + (H - E - F + G)*0.5;
	res[1] = A - (E + F + G + H)*0.5;
	res[2] = C + (E - F + G - H)*0.5;
	res[3] = D + (E - F - G + H)*0.5;
}

Don't forget to put their prototypes in q_shared.h:

void AnglesToQuat (const vec3_t angles, vec4_t quat);
void QuatToAxis(vec4_t q, vec3_t m[3]);
void QuatMul(const vec4_t q1, const vec4_t q2, vec4_t res);

Also put the following macro definition in q_shared.h:

#define QuatInit(w,x,y,z,q) ((q)[0]=(w),(q)[1]=(x),(q)[2]=(y),(q)[3]=(z));

We need a place to store the new quaternions so open cg_local.h and add the following to the definition of the localEntity_t struct:

	vec4_t quatOrient;
	vec4_t quatRot;
	vec3_t rotAxis;
	float angVel;

Now open cg_localents.c and find the function CG_ReflectVelocity(). Inside this function add the following code at the end:

	if (le->leFlags & LEF_TUMBLE)
	{
		// collided with a surface so calculate the new rotation axis
		CrossProduct (trace->plane.normal, velocity, le->rotAxis);
		le->angVel = VectorNormalize (le->rotAxis) / le->radius;
		// save current orientation as a rotation from model's base orientation
		QuatMul (le->quatOrient, le->quatRot, le->quatRot);
		// reset the orientation
		QuatInit(1,0,0,0,le->quatOrient);
	}

Next, scroll down to the function CG_AddFragment() and find the code:

	if ( le->leFlags & LEF_TUMBLE ) {
		vec3_t angles;
	
		BG_EvaluateTrajectory( &le->angles, cg.time, angles );
		AnglesToAxis( angles, le->refEntity.axis );
	}

And replace it with this:

	if (le->leFlags & LEF_TUMBLE)
	{
		vec4_t qrot;
		// angular rotation for this frame
		float angle = le->angVel * (cg.time - le->angles.trTime) * 0.001/2;
		// create the rotation quaternion
		qrot[0] = cos (angle); // real part
		VectorScale (le->rotAxis, sin(angle), &qrot[1]);// imaginary part
		// create the new orientation
		QuatMul (le->quatOrient, qrot, le->quatOrient);
		// apply the combined previous rotations around other axes
		QuatMul (le->quatOrient, le->quatRot, qrot);
		// convert the orientation into the form the renderer wants
		QuatToAxis (qrot, le->refEntity.axis);
		le->angles.trTime = cg.time;
	}

Open cg_effects.c and find the function CG_LaunchGib(). At the end of this function add the following code:

	le->angles.trType = TR_LINEAR;
	le->angles.trTime = cg.time;
	le->angVel = 20 * crandom(); // random angular velocity
	le->rotAxis[0] = crandom(); // random axis of rotation
	le->rotAxis[1] = crandom();
	le->rotAxis[2] = crandom();
	VectorNormalize (le->rotAxis); // normalize the rotation axis
	QuatInit (1,0,0,0,le->quatRot);
	QuatInit (1,0,0,0,le->quatOrient);
	le->radius = 12;
	le->leFlags = LEF_TUMBLE;

Open cg_weapons.c and find the function CG_MachineGunEjectBrass(). Find the lines:

	le->angles.trDelta[0] = 2;
	le->angles.trDelta[1] = 1;
	le->angles.trDelta[2] = 0;

And replace them with:

	AnglesToQuat (le->angles.trBase, le->quatOrient);
	le->angVel = 10 * random();
	le->rotAxis[0] = crandom();
	le->rotAxis[1] = crandom();
	le->rotAxis[2] = crandom();
	VectorNormalize (le->rotAxis);
	le->radius = 4;
	QuatInit (1,0,0,0,le->quatRot);

Scroll down to the function CG_ShotgunEjectBrass() and find the lines:

	le->angles.trDelta[0] = 1;
	le->angles.trDelta[1] = 0.5;
	le->angles.trDelta[2] = 0;

And replace them with:

	AnglesToQuat (le->angles.trBase, le->quatOrient);
	le->angVel = 10 * random();
	le->rotAxis[0] = crandom();
	le->rotAxis[1] = crandom();
	le->rotAxis[2] = crandom();
	VectorNormalize (le->rotAxis);
	le->radius = 6;
	QuatInit (1,0,0,0,le->quatRot);
That's it. You're done. Hopefully I haven't left anything out. Compile the cgame module and try it out. You may want to play around with the le->bounceFactor and le->endTime parameters in the object spawning functions so that the objects stay airborn longer. Also, I found it useful to put the game into slow motion so it is easier to see the object motion while debugging. You can do this by modifying the timescale cvar. Values between 0.5 and 0.7 worked good for me. You also might want to temporarily increase the damage in G_Damage() to really get the gibs a-flyin'. I left out the function to convert a quaternion back to Q3's Euler angle because it wasn't needed in this example. Just in case you're curious or want to experiment further here it is:

void QuatToAngles (const vec4_t q, vec3_t a)
{
	vec4_t q2;
	q2[0] = q[0]*q[0];
	q2[1] = q[1]*q[1];
	q2[2] = q[2]*q[2];
	q2[3] = q[3]*q[3];
	a[ROLL] = (180.0/M_PI)*atan2 (2*(q[2]*q[3] + q[1]*q[0]) , (-q2[1] - q2[2] + q2[3] + q2[0]));
	a[PITCH] = (180.0/M_PI)*asin (-2*(q[1]*q[3] - q[2]*q[0]));
	a[YAW] = (180.0/M_PI)*atan2 (2*(q[1]*q[2] + q[3]*q[0]) , (q2[1] - q2[2] - q2[3] + q2[0]));
}
Original post: http://www.quake3world.com/ubb/Forum4/HTML/006766.html?
Converted into HTML by glossy
glossyshit@hotmail.com

Mangled with CSS by PhaethonH
PhaethonH@gmail.com

Permission has been granted by JackOfAllTrades to make verbatim copies of this document and make modifications as needed (which allowed this marked-up version).