Euler Angles
https://en.wikipedia.org/wiki/Euler_angles
Quaternions
https://en.wikipedia.org/wiki/Quaternion
Angles in Orbital Aero Model
Using Quaternions is actually fairly simple, but getting a solution off the Internet that "just works" is more difficult than it should be. The options were either gigantic libraries that I only needed a tiny piece of, smaller convoluted source code snippets, or straightforward code snippets that actually failed under certain circumstances!
My solution was to take the best pieces of several sources and simplify them down to just what I needed. A way to represent an orientation, a way to rotate the object in space, and a way to rotate a vector (such as thrust) by an orientation.
Body Axis
x forward
y right
z down
For my Euler angles, I'm using the convention of rotation of z followed by y followed by x. This matches the IEEE 1278.1 DIS (Distributed Interactive Simulation) standard used by military simulation networks (easy for me to communicate with those simulations).
A body is represented in space by its location vector from the world axis and then the rotation from the world to the body axis.
Using the Rotation Code
The Body class was modified to contain an orientation with the Quaternions class and an angular velocity with the Euler angles class:
class BodyModel
{
.
.
Quaternion orientation_quaternions;
Euler angularVelocity_radiansPerSecond;
};
In the body's constructor, I clear out the orientation and the angular velocity:
this->orientation_quaternions.clear();
this->angularVelocity_radiansPerSecond.clear();
Quaternion.clear() sets the Quaternions values to: w=1, x=0, y=0, and z=0 which is the equivalent to an Euler angle of 0,0,0 (no rotation).
I'm using SFML (Simple Fast Media Library) to get joystick inputs to control one of the bodies. Each frame I get the inputs and store them in the orbital aero object. SFML returns axis positions +/- 100, and I'm using a 10% dead zone so I don't have excess twitching when things should be static.
sf::Joystick::update();
if (sf::Joystick::isConnected(0))
{
float joystickDeadZone = 10;
float joystickX = sf::Joystick::getAxisPosition(0, sf::Joystick::X);
float joystickY = sf::Joystick::getAxisPosition(0, sf::Joystick::Y);
float joystickR = sf::Joystick::getAxisPosition(0, sf::Joystick::R);
float joystickZ = sf::Joystick::getAxisPosition(0, sf::Joystick::Z);
if (abs(joystickZ) > joystickDeadZone)
{
orbitalAero.joystickInputs.roll = (joystickZ - (joystickDeadZone * sgn(joystickZ))) / (100 - joystickDeadZone);
}
else
{
orbitalAero.joystickInputs.roll = 0.0;
}
if (abs(joystickR) > joystickDeadZone)
{
orbitalAero.joystickInputs.pitch = (joystickR - (joystickDeadZone * sgn(joystickR))) / (100 - joystickDeadZone);
}
else
{
orbitalAero.joystickInputs.pitch = 0.0;
}
if (abs(joystickX) > joystickDeadZone)
{
orbitalAero.joystickInputs.yaw = (joystickX - (joystickDeadZone * sgn(joystickX))) / (100 - joystickDeadZone);
}
else
{
orbitalAero.joystickInputs.yaw = 0.0;
}
if (abs(joystickY) > joystickDeadZone)
{
orbitalAero.joystickInputs.thrust = -(joystickY - (joystickDeadZone * sgn(joystickY))) / (100 - joystickDeadZone);
}
else
{
orbitalAero.joystickInputs.thrust = 0.0;
}
} // Joystick::isConnected(0)
Then in the body's control system called each frame (not described yet), I assign angular velocities and thrust based on the joystick inputs. The following / 10.0 is just a scaling so the body doesn't rotate too quickly for a given input.
hostBody.angularVelocity_radiansPerSecond.phi = hostOrbitalAero.joystickInputs.roll / 10.0;
hostBody.angularVelocity_radiansPerSecond.theta = hostOrbitalAero.joystickInputs.pitch / 10.0;
hostBody.angularVelocity_radiansPerSecond.psi = hostOrbitalAero.joystickInputs.yaw / 10.0;
Currently I'm changing the angular velocities directly based on the joystick, but in the near future I'll model some type of thrusters / other mechanisms to give an off-axis thrust that will cause the body to rotate. I need to get moment of inertia in for that!
Joystick thrust is considered to be along the body x (forward) axis; however for the processForces calculations later I need that thrust along the world axis.
The following code converts the thrust from the body axis to the world axis via the .rotatedBy call. (If the two axis are aligned, then those vectors will be the same. But if the body is turned at some angle relative to the world axis, then the body's thrust vector will also be rotated by the same angle.)
The following code converts the thrust from the body axis to the world axis via the .rotatedBy call. (If the two axis are aligned, then those vectors will be the same. But if the body is turned at some angle relative to the world axis, then the body's thrust vector will also be rotated by the same angle.)
double thrust_newtons = hostOrbitalAero.joystickInputs.thrust * 1000.0;
// All thrust is along the x (forward) axis of the entity.
// Convert thrust from local axis to world axis.
Vector thrustBody_newtons(thrust_newtons, 0, 0);
hostBody.forceThrust_newtons = thrustBody_newtons.rotatedBy(hostBody.orientation_quaternions.inverse());
// Alternate way to rotate the vector using Quaternions converted to Euler angles.
//hostBody.forceThrust_newtons = thrustBody_newtons.rotatedBy(hostBody.orientation_quaternions.inverse().toEulerAngles());
// Function to loop through all bodies and sum the forces acting on them, including forces from
// thrust, gravity, atmospheric drag, and friction.
bool OrbitalAeroModel::processForces()
{
// Loop through all bodies and calculate their forces upon each other.
// The outer loop is for the primary body that we're calculating forces for.
map <uint64, BodyModel>::iterator iteratorPrimaryBody = tableBodies.begin();
while (iteratorPrimaryBody != tableBodies.end())
{
BodyModel &primaryBody = iteratorPrimaryBody->second;
// Only process the primary body if it's active.
if (primaryBody.active)
{
// Create a vector to contain the sum of all forces.
Vector sumForces_newtons;
// Start with the thrust force of any onboard engines.
sumForces_newtons = primaryBody.forceThrust_newtons;
.
.
// Translate and rotate bodies based on their mass, moments of inertia, and forces.
bool OrbitalAeroModel::processKinematics(double timeStep_seconds)
{
// Loop through all bodies and reposition them based on the forces.
map <uint64, BodyModel>::iterator iteratorPrimaryBody = tableBodies.begin();
while (iteratorPrimaryBody != tableBodies.end())
{
BodyModel &primaryBody = iteratorPrimaryBody->second;
if (primaryBody.active)
{
.
.
// Rotation
if (primaryBody.angularVelocity_radiansPerSecond.isNonZero())
{
// Rotation around local axis.
primaryBody.orientation_quaternions.rotateAboutLocalAxis(primaryBody.angularVelocity_radiansPerSecond * timeStep_seconds);
}
} // (primaryBody.active)
iteratorPrimaryBody++;
} // (iteratorPrimaryBody != tableBodies.end())
return true;
}
The angular velocities are relative to the local axis of the entity. So when I do my Quaternion rotation, I call the rotateAboutLocalAxis function (as opposed to rotateAboutWorldAxis).
Testing
It has been fun modeling a spacecraft over the Earth and flying it around with the joystick. I'll try to get a video up.
Rotation Classes
Rather than overload this post with both the approach and class code, I'll post my Vector, Euler, Quaternion, and Matrix code in the next few posts.
Copyright (c) 2017 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
No comments:
Post a Comment