Saturday, January 24, 2015

Orbital Aero Model: Vector Model

** Update 21 Jan 2017: This Vector class has been deprecated, new version posted here. **

I created a Vector class that is used extensively in the code. Absolute locations, relative locations, velocities, accelerations, and forces all use this same class to hold values in 3 dimensional space. My vector is a simple 1 dimensional array of 3 values. Using an array makes things easier when dealing with matrix math (which will be introduced later). Also keeping it generic means those values could be considered an x/y/z or phi/theta/psi (orientation).

Operator Overloading


Operator overloading allows the programmer to use standard math operators on structures or classes that the compiler would normally not know what to do with. I use them extensively with vectors in the Orbital Aero Model; it helps to both condense the code and make it more readable.

For example, I'm trying to add vectors b and c together to store them in a.

Instead of writing code such as:
a.x = b.x + c.x;
a.y = b.y + c.y;
a.z = b.z + c.z;

I could write:
a = b + c;

The overload for that case would be:
Vector Vector::operator+(const Vector &vectorToAdd)
{
return Vector(this->value[0] + vectorToAdd.value[0], this->value[1] + vectorToAdd.value[1], this->value[2] + vectorToAdd.value[2]);
}

The addition operation is being performed on vector b (this) and vector c (vectorToAdd). It is returning a new vector of the sum that is stored in a.

A similar example is to add the value of b into a.

Instead of writing code such as:
a.x += b.x;
a.y += b.y;
a.z += b.z;

I could write:
a += b;

The overload for that case would be:
Vector &Vector::operator+=(const Vector &vectorToAdd)
{
this->value[0] += vectorToAdd.value[0];
this->value[1] += vectorToAdd.value[1];
this->value[2] += vectorToAdd.value[2];
return *this;
}

The addition operation is being performed on vector a (this) and vector b (vectorToAdd). The summed value is stored directly in a.

Since so much of the code uses my vector class that has operator overloading, I figured it would be best to share that class first. As I extend the functionality of my vector math, I'll update this post to include the latest additions.

More Resources


For more information on vector math, I recommend learning the following concepts:
Vector
Dot Product
Cross Product

If you've never seen a Dot Product or Cross Product before, you may be questioning what they're for. They're actually both VERY important concepts when doing 3-dimensional math. You'll see examples of them in action in the code that follows.

VectorModel.h


#pragma once

class Vector
{
private:

public:
double value[3];

Vector();

Vector(double value0, double value1, double value2);

Vector &Vector::operator=(double newValue);

Vector &Vector::operator=(const Vector &thatVector);

Vector Vector::operator-();

Vector Vector::operator+(const Vector &vectorToAdd);

Vector Vector::operator-(const Vector &vectorToSubtract);

Vector Vector::operator*(double valueToMultiply);

Vector Vector::operator*(const Vector &vectorToMultiply);

Vector Vector::operator/(double valueToDivide);

Vector &Vector::operator+=(const Vector &vectorToAdd);

Vector &Vector::operator-=(const Vector &vectorToSubtract);

Vector &Vector::operator*=(double valueToMultiply);

Vector &Vector::operator/=(double valueToDivide);

double magnitude();

Vector unit();

Vector sign();

static double dotProduct(Vector vector1, Vector vector2);
};


VectorModel.c


#include "math.h"
#include "VectorModel.h"

Vector::Vector()
{
this->value[0] = 0.0;
this->value[1] = 0.0;
this->value[2] = 0.0;
return;
}

Vector::Vector(double value0, double value1, double value2)
{
this->value[0] = value0;
this->value[1] = value1;
this->value[2] = value2;
return;
}

Vector &Vector::operator=(double newValue)
{
this->value[0] = newValue;
this->value[1] = newValue;
this->value[2] = newValue;
return *this;
}

Vector &Vector::operator=(const Vector &thatVector)
{
// Protect against self-assignment. (Otherwise bad things happen when it's reading from memory it has cleared.)
if (this != &thatVector)
{
this->value[0] = thatVector.value[0];
this->value[1] = thatVector.value[1];
this->value[2] = thatVector.value[2];
}
return *this;
}

Vector Vector::operator-()
{
return Vector(-this->value[0], -this->value[1], -this->value[2]);
}

Vector Vector::operator+(const Vector &vectorToAdd)
{
return Vector(this->value[0] + vectorToAdd.value[0], this->value[1] + vectorToAdd.value[1], this->value[2] + vectorToAdd.value[2]);
}

Vector Vector::operator-(const Vector &vectorToSubtract)
{
return Vector(this->value[0] - vectorToSubtract.value[0], this->value[1] - vectorToSubtract.value[1], this->value[2] - vectorToSubtract.value[2]);
}

Vector Vector::operator*(double valueToMultiply)
{
return Vector(this->value[0] * valueToMultiply, this->value[1] * valueToMultiply, this->value[2] * valueToMultiply);
}

Vector Vector::operator*(const Vector &vectorToMultiply)
{
return Vector(this->value[0] * vectorToMultiply.value[0], this->value[1] * vectorToMultiply.value[1], this->value[2] * vectorToMultiply.value[2]);
}

Vector Vector::operator/(double valueToDivide)
{
return Vector(this->value[0] / valueToDivide, this->value[1] / valueToDivide, this->value[2] / valueToDivide);
}

Vector &Vector::operator+=(const Vector &vectorToAdd)
{
this->value[0] += vectorToAdd.value[0];
this->value[1] += vectorToAdd.value[1];
this->value[2] += vectorToAdd.value[2];
return *this;
}

Vector &Vector::operator-=(const Vector &vectorToSubtract)
{
this->value[0] -= vectorToSubtract.value[0];
this->value[1] -= vectorToSubtract.value[1];
this->value[2] -= vectorToSubtract.value[2];
return *this;
}

Vector &Vector::operator*=(double valueToMultiply)
{
this->value[0] *= valueToMultiply;
this->value[1] *= valueToMultiply;
this->value[2] *= valueToMultiply;
return *this;
}

Vector &Vector::operator/=(double valueToDivide)
{
this->value[0] /= valueToDivide;
this->value[1] /= valueToDivide;
this->value[2] /= valueToDivide;
return *this;
}

double Vector::magnitude()
{
return sqrt( (this->value[0] * this->value[0]) + (this->value[1] * this->value[1]) + (this->value[2] * this->value[2]) );
}

Vector Vector::unit()
{
double mag = magnitude();
Vector unitVector;
unitVector.value[0] = this->value[0] / mag;
unitVector.value[1] = this->value[1] / mag;
unitVector.value[2] = this->value[2] / mag;
return unitVector;
}

Vector Vector::sign()
{
Vector signVector;
signVector.value[0] = sgn(this->value[0]);
signVector.value[1] = sgn(this->value[1]);
signVector.value[2] = sgn(this->value[2]);
return signVector;
}

double Vector::dotProduct(Vector vector1, Vector vector2)
{
return ((vector1.value[0] * vector2.value[0]) + (vector1.value[1] * vector2.value[1]) + (vector1.value[2] * vector2.value[2]));
}


Copyright (c) 2015 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.

Thursday, January 22, 2015

Orbital Aero Model: Intro

Back in college (a little over a decade ago), I made an orbital mechanics / space simulation into a MUD (Multi User Dungeon / Multi User Dimension). I used SocketMUD as a base and added in the concept of "bodies" that interact with each other. The bodies would pull on one another with their gravity (based on mass) and could bounce off each other (based on mass and coefficient of restitution). There was also a simple implementation of electromagnetic waves that could originate from a body and propagate through space to impact other bodies (secondary bodies would cause shadows in the electromagnetic wave).

I think I had the sun, planets, Earth's moon, and a couple spacecraft in continuous motion (real time). Users could log in, look at positions and velocities of the bodies, create new bodies, and delete existing bodies. For the year or so I had the MUD running, I think I had one other person log in and play around with the simulation. So I can't say it was the most popular, but most of the time MUDs are meant for killing goblins and leveling up - not watching x/y/z coordinates change.

The original code worked for what it was, but you can learn a lot about coding techniques in 10 years. So this past year I've been dusting off the old stuff and bringing it back to life. It's no longer a MUD, but I do have a simple visualizer...


In this video you can see several spacecraft orbiting the Earth (currently a perfect sphere, but that will change too). The spacecraft nearest to the Earth are actually being dragged down because they're within the Earth's atmosphere. Later in the video you can see a spacecraft orbiting the moon which is orbiting the Earth. I find the history trails of the spacecraft orbiting the moon to be very interesting.

Why call it Orbital Aero?


I want to continue to build as realistic simulation as possible for vehicles travelling on planets, through the atmosphere of planets, and through space. Hence orbital for space and aero for atmospheric effects near a planet. The goal would be to accurately simulate a spacecraft taking off from the Earth, travelling through space, and landing on Mars. (Or to a planet in another solar system!)

80% Solution


I'm developing based on what I'll call the "80% solution". To get near a 100% accurate simulation is extremely expensive (both in development hours and CPU clock cycles). However, to get an 80% accurate simulation isn't nearly as difficult and usually suffices for most customers needs. (Obviously what is "80%" is very subjective.)

I'm going to tackle one concept of the simulation and get it to what I consider 80%, then move on to the next concept. As I go around on the different parts of the simulation (atmospheric modeling or physical shapes of bodies), I'll revisit previous code and essentially redefine 80%.

For example: Right now every body in the simulation is a perfect sphere. To have basic spacecraft orbit a planet, transfer between planets, or escape the solar system, it's good enough. That's what the simulation can do now. However to do interactions between spacecraft and a planet (GPS modeling for example), it's important to have an ellipsoid shape Earth (such as the WGS-84 model). So when the goal is to do GPS, what suffices for 80% is redefined and work will have to be completed to reach that point. To further this point, a WGS-84 round Earth model will suffice for many requirements, but if you're trying to drive a car on the surface of a planet its woefully inadequate. At that point you'll need to load some kind of elevation data such as SRTM (Shuttle RADAR Topology Mission) or DTED (Digital Terrain Elevation Data).

A little background on myself... I got my degree in Aerospace Engineering and have been working on simulation software professionally ever since. This is another long term project. I'll be posting updates on the simulation and the physics behind it as it progresses. If you have questions, comments, suggestions for improvements, or reports of errors then please let me know! (Side note, the Arduino computer is still in the works. I said I've got too many projects!)

Saturday, January 3, 2015

Arduino Retro Computer: Second Joystick

I've been able to build and wire up my second joystick!


Learning some lessons from the first version, I did redesign the lid so it fits better into the case. Rather than the prongs in the corners, the lid now has 4 tabs along the top and bottom of the joystick. I can drill holes through the joystick case and into the tabs to add a set screw to keep the lid in position (in the picture above you can see a set screw at the top left and bottom right of each controller).


The guts of the two controllers...


I printed a new lid for the first joystick as well so they're the same. The wiring inside the two controllers are identical. I did change the pin layout at the Arduino for joystick #1 so it would be grouped with joystick #2. My new pin layout:

const int Joystick1SelectPin = 40;
const int Joystick1RightPin = 41;
const int Joystick1TopPin = 42;
const int Joystick1LeftPin = 43;
const int Joystick1BottomPin = 44;
const int Joystick1AnalogX = 0; // Analog 0
const int Joystick1AnalogY = 1; // Analog 1

const int Joystick2SelectPin = 45;
const int Joystick2RightPin = 46;
const int Joystick2TopPin = 47;
const int Joystick2LeftPin = 48;
const int Joystick2BottomPin = 49;
const int Joystick2AnalogX = 2; // Analog 2
const int Joystick2AnalogY = 3; // Analog 3


Initializing my joysticks with the Joystick Class (described in the previous post):

  joysticks[0].init(Joystick1SelectPin, Joystick1RightPin, Joystick1TopPin, 
                    Joystick1LeftPin, Joystick1BottomPin, Joystick1AnalogX, Joystick1AnalogY);
  joysticks[1].init(Joystick2SelectPin, Joystick2RightPin, Joystick2TopPin, 
                    Joystick2LeftPin, Joystick2BottomPin, Joystick2AnalogX, Joystick2AnalogY);

And of course someone became very interested while I was doing the final wiring.