Monday, December 18, 2017

Raspberry Pi Server: SVN Upgrade

My original plan was to upgrade my Raspberry Pi server to a new Pi 3, and use the original Pi 1 B+ for the Retro Pie. I've since backtracked for various reasons and put the Retro Pie SD card into the new Pi 3. So now my original server is back on the Pi 1 B+. The server had been working great except it was running out of disk space (only had a 4 GB SD Card).

Fairly simple to solve, I'll just use an external USB drive for storage! I plugged in a 32 GB USB drive, and now I needed to move my repository.

First I needed a folder to act as my reference location when I wanted to access the USB drive. It doesn't matter too much where, I went with: /media/usb

Created the folder with: sudo mkdir /media/usb
Set the ownership of the folder with: sudo chown pi /media/usb
Set the permissions of the folder with: sudo chmod 0777 /media/usb


After plugging in the USB device, I needed to know the device UUID by typing: sudo blkid


Then I needed to edit /etc/fstab for it to automount on boot: sudo nano /etc/fstab


For my new mount, I went with:
UUID=390A1652      /media/usb      vfat    uid=pi,gid=pi,umask=0000,sync,auto,nosuid,rw,nouser,nofail 0 0


As an alternative, I could specify to automount any USB drive that is plugged in as /dev/sda1. That seemed risky to me though as my current drive could change and the new drive could connect as /dev/sda1. That new drive wouldn't necessarily be vfat.

If I had wanted to mount any USB drive, the line would have read:
/dev/sda1        /media/usb      vfat    uid=pi,gid=pi,umask=0000,sync,auto,nosuid,rw,nouser,nofail 0 0

Lots of options and opinions on this:
uid & gid are my username; some references suggest entering the numeric value. You can check your account's id by typing: id -u and id -g. Turned out my pi user was 1000.
umask=0000 is the equivalent of chmod 0777 (everyone has read/write access).
sync has input and output be synchronously.
auto instructs the drive to mount automatically at bootup.
nosuid I don't understand, but one reference says: Block the operation of suid, and sgid bits.
rw to mount the drive with read-write access.
nouser permits only root to mount the filesystem.
nofail causes startup to not wait 90 seconds / error out if it fails to find the drive.

And finally reboot the Pi via: sudo reboot

The next task was to move my existing SVN repository to the external drive...

I stopped Apache which is used for SVN: sudo /etc/init.d/apache2 stop


I copied the repository over: cp -r repos /media/usb


Then I modified the Apache configuration to point to the new location of the SVN repository. The config file is located in /etc/apache2/mods-available, file dav_svn.conf: sudo nano /etc/apache2/mods-available/dav_svn.conf


My SVNParentPath was /home/pi/repos, I changed it to /media/usb/repos


Apache actually needs ownership of the repository folder, so I changed ownership to www-data: sudo chown -R www-data:www-data /media/usb/repos


And finally, restart Apache: sudo /etc/init.d/apache2 restart


I did a full restart to make sure everything came back up, and it did! So my SVN repository now has 32 GB to play with; far better than the few hundred MB from before.


The above instructions were mostly just for my reference in case I need to do this or similar again in the future, but I hope it can be of help to others!


Monday, December 11, 2017

Retro Pie

I've been playing some classic games on my laptop with emulators, but it's just not the same as on the T.V.. I had some time over the Thanksgiving holiday to do something about it. I've read many other sites using Retro Pie and decided to go with that.

I'm reusing my original Pi B+, but its existing 4 GB SD Card was inadequate for the task. So I picked up a new 16 GB card...



Downloaded Retro Pie off the website... Their instructions were fantastic: https://retropie.org.uk/docs/First-Installation/




Then got Win32 Disk Imager...


And used it to write the image to my SD card...


On the left is my existing Pi in its case, and on the right is the "Super Tinytendo" case I bought off Amazon for $20. The build quality on the Tinytendo is great. (From the reviews, it sounds like their first versions with rough 3-D prints. The one I bought however was injection modeled and of very high quality.) 


4 screws underneath to open the case...


And the Pi fits perfectly inside...


The power light in front is functional; the instructions were very simple and spelled out in the Tinytendo manual. The Power and Reset buttons are unfortunately not functional. It'd be interesting to make them so though.


I ended up overclocking my Pi B+ so it could handle SNES games better (more on that below). So out of precaution I added a heat sync from Adafruit over the CPU.


First boot!


RetroPie only shows emulators for the corresponding games it finds. So at first boot no emulators are shown; you have to load them up. ROMs can be added via USB, SFTP, or Samba share. Since I'm primarily a Windows user (uhg), I went the Samba route. The Samba Share was super easy to access and copy ROMs to. Just type \\retropie in file explorer and you're there!


After a reboot the appropriate emulators showed up!


Some of the SNES games were running a bit slow, so I decided to try overclocking. You can access the Raspberry Pi config options through the Retro Pie menu.


Selected the overclock option...


And set the overclock to Medium. The games are working reasonably well at this level so I'm hoping this will suffice.


I'm using two Logitech Gamepads for the controllers. They have a good layout for both NES and SNES systems (among others), but they also have a few additional buttons not used by those earlier consoles. I'm using the rear-most left button as my hotkey button. If I push hotkey + rear left then it loads my last save state; hotkey + rear right saves my current state, and hotkey + start quits the current game.


The Pi rounds up a nice collection for entertainment - Switch (newest) on the left, N64 (the best) in the middle, and NES/SNES (the classic) on the right.


Thursday, November 23, 2017

RPG: Map

Unfortunately the idea of using real world photos for a RPG was short lived. I continued for another couple weeks but the shortfalls just too heavily outnumbered the benefits.

Biggest upside of the photos was the beautiful static images the game could have, but you want the world to feel alive. I did have some limited success with rain effects and varying the lighting to give a little bit more movement into the world, so that probably could have been sufficiently resolved. However I had 2 problems that could not be overcome...

1. If I wanted to setup a shop, inn, or whatever fantasy style building, I had to hope it already existed on the trails (which it wouldn't be). Otherwise it would be a huge amount of photoshopping to add a building realistic enough to blend into the scene to not just one photo but several photos at a variety of angles and distances (with vegetation / scenery blocking parts of the building)..

2. Movement had to be in discrete steps. (In my prototype I had taken photos roughly every 20 feet, which is fine for turn based but not for real time.) And trying to merge multiplayer and turn based in what is essentially a first person dungeon crawler just doesn't work out. My son can play Diablo style games, so if I want him to enjoy the game with me I need something real time.

And thus, the game has been redesigned! Thankfully not much code was thrown out. In fact what the game is now is what I had originally played around with when first learning SFML several years ago. So I replaced the real-world location code with the "whole world" map code I had written before.

It looks much more like a modern action role playing game:


Please forgive my awful developer art. I'm focusing on code now!

You can walk around with WASD. Unexplored areas are completely gray, but there is a fog of war for areas you've previously explored (shaded darker). Characters such as yourself, trees, or buildings can be on the game map. Also equipment can be dropped onto or picked up from the map. I have draw order working nicely - you can walk behind trees for example. The game prevents you from walking through trees (based on a radius) or buildings (based on a width/depth).


In the above screenshot, I've picked the sword up off the ground. I can now equip it or put it in my backpack.

You can use the mouse scroll wheel to zoom in or out of the map.


You can actually zoom out enough to see the entire world...


A player can traverse the entire globe. For the past several weeks I've been steadily making improvements, and I'm really liking the results. Terrain tiles and characters are animated. (Water can have waves, trees can sway in the breeze, and the player's feet move as you walk around.)

At this point I've got a very solid plan for the game.

The 5 second pitch is: Growing up you had the dream of mapping the entire world. And now that you're older, you've decided to do just that. But quite a lot of things can happen to someone exploring a whole world...

So the scope has increased, but a playable version shouldn't take too much longer. I can create a world with monsters to fight, inns to stay at, etc. relatively quickly. But this also gives me room to go much further. I could have warring kingdoms that you could join sides in or try to resolve in other ways. I could have the classic evil mage in the tower threatening nearby lands you may want to defeat. I could have a small farm community that you run across and decide is where you want to put down your roots. (Inspired by Stardew Valley...) But I don't want to get into too much scope creep yet; for now it's build a world you can run around in and kill monsters & improve yourself.

Tuesday, October 24, 2017

Rocket Launcher


Launching Estes rockets is fun; really fun. When I was younger I had a proper launch setup with rocket stand and handheld launcher. At some point those got lost and/or thrown out. The past few years to launch a rocket I just grabbed a long coil of wire and a 9-volt battery. Connect the wire to the Estes igniter, hold the battery against the wires, and lift off!

I'd like my son to have a slightly more... authentic(?) experience.

For the launcher, I want a nicely packaged box, with "ready" indicator light, launch button, and most importantly a safety key switch. That way I can hold onto the key while hooking the wires up to the igniter (without worrying about my son pressing launch prematurely).

Other than the key switch, all the other parts were leftovers from prior projects.


Drilled the holes for the key switch, light, and push button.


Quick and easy crimp the wires together. (There is a hole in the side for the long wire that goes out to the rocket.)


Wiring diagram for those that can't follow the above mess...


When the key is turned, the light illuminates signalling that the rocket is armed. Press the red button and electricity goes out to the igniter to launch.


And it works! There was basically no wind so it landed feet from the launch site.


Next up is a proper launch stand!

Friday, September 29, 2017

RPG: Intro


I wanted to make a follow on to my Pebble watch RPG, but since the company is now gone that would not be the best use of my time. I still however have a desire to make the kind of RPG I want to play. And during a nice hike with the family one day, I decided on the type of RPG to make!

Two RPGs that I spent by far the most time on and enjoyed the most are Diablo 1 and a MUD called Dreamshadow. I played through Diablo 2 and 3, but I think the first one is best. (Please note, my opinions on Diablo 3 are before Loot 2.0 so I understand they fixed many of the issues with the PC version of the game.)

Customization
In Diablo 1, you pick from one of three classes which bounds your stats, but you really have complete freedom beyond that. Your rogue can cast spells and your mage can wield swords. Straying from your initial class definitely puts you at a handicap, but it's still an option.
In Diablo 2, you have to pick a development path for your character and that's it. To even experiment with other abilities of the same class you have to create a new character.
In Diablo 3, well, there is no customization. You have what everyone else will have at your level.

Death
In Diablo 1, you have actual tense moments because of the consequences of death. (You really don't want the butcher to kill you, because then you have to go back down there to fight him naked to get your stuff back - at least in multiplayer mode.)
In Diabo 2, it's very similar but I remember you being able to re-equip yourself in a single click rather than Diablo 1's managing items one by one.
In Diablo 3, well, you come back to life a trivial distance away that you can usually walk back to in about 10 seconds. A minor inconvenience.

Items
In Diablo 1, getting a magical item was a big deal. That was excitement. And not only that, you absolutely had to identify that item before using it because it could be cursed and make you worse.
In Diablo 3, well, freaking everything drops a mountain of magical items. And it's so bad excessive are multiple level of magical items such as rare and legendary. After killing some monsters it's a chore sifting through which magical items are even worth messing with. And were there even any cursed magical items?

Levels & Skills
The Dreamshadow MUD did not use levels. Instead, it had this fantastic hierarchy based skill system that was both very realistic and very rewarding. I'll go into those details when I code it, but it is a must have.

So with my game, I want your character to have no arbitrary limits. You can be as well rounded or specialized as you want. With death, I don't want people to rage quit, but I also want them to really NOT want to die. And finally a magical item is something especially rare and special. I have many other design ideas for the game, but I'll go into those details as I address them in the code.

I like an isometric view over a first-person view in RPGs, but being on the hike game me an interesting idea. Rather than going with retro 8-bit graphics or fancy 3-D generated worlds, my RPG could consist of hundreds of actual photos of real-world places. Basically the adventurer will be fighting in real world areas - along actual state and national park trails. Most hiking trails don't have too many branches that go off of them, but I think my Pebble game was still successful where you had no ability to veer off the single path anyways. So perhaps it could work.


I went along a nearby state park to take many pictures in various directions. For a proof of concept, I just took pictures with my phone camera, that has a scratch over the lens, in one hand, while carrying my toddler in my other arm, while said toddler was trying to reach for said camera. It's a prototype...

A few things were evident right away... need to take pictures at high noon, might need to take pictures when it's overcast, and the locations & orientations of each photo need to be recorded immediately upon taking them. All things that can be addressed on a second outing to the park if the prototype game looks worth pursuing.

One important piece is I want to make the still pictures feel as alive as possible. I'm experimenting with graphical effects to accomplish that.

I've got the torch effect when it gets dark looking pretty good... (intensity flickers)


And I just implemented some rain... (looks better animated)


I'm using SFML (Simple Fast Media Library) as my only third party library right now. It should cover everything I need. I don't have any code that I want to post now, but I'm writing my own presentation library to handle the drawing and user controls. It's coming along nicely and will likely be valuable to others when it's ready to share.

Whereas the Orbital Aero is a very long term multi-year project, I'd like to have a playable version of this "Trail RPG" within a few months. (Honestly it's not that development takes that much time, it's just my limited time to do it.) So Orbital Aero is still in active development! But I'll be bouncing back and forth between it and this game.

Wednesday, August 30, 2017

Orbital Aero Model: Kinematics (Rotation)

In the first Kinematics post, I went into how bodies are moved in space. This post will go into how bodies are rotated in space.


The amount a body will rotate is not based on force and mass, but instead it is based on the moment applied to the body (the Torque, T) and the body's moment of inertia (I).

Torque


https://en.wikipedia.org/wiki/Torque


Torque (moment) is calculated from the equation:


T is the calculated torque applied.
r is the perpendicular distance from the axis of rotation to the force applied.
F is the force applied.

A simple way of thinking about moment is a wrench tightening a bolt.

If the wrench was 1 foot long with 10 pounds of pressure applied to the end of it, then the calculated moment is:
T = 1 * 10 = 10 foot-pounds of torque

If the wrench was twice as long but the same force applied, then the calculated moment is:
T = 2 * 10 = 20 foot-pounds of torque

Which demonstrates the entire purpose of having long handles on wrenches! They allow people to tighten bolts with less force.

Since the entire simulation (and most of the world) uses metric, I'll be using metric for the rest of this post and the code. So our moment units will be Newton-meters rather than foot-pounds.

For a metric example the wrench is 0.25 meters long with 50 newton of force applied, the calculated moment is:
T = 0.25 * 50 = 12.5 Newton-meters of torque

The torque equation I gave above was just for a single axis plane. The equation to cover a vector force in multiple dimensions uses the cross product:


This equation takes a vector force, F, and a vector position, r, to get the resulting torque, T, that is applied in all 3 axes.


In this diagram, a force vector, F, is applied at the corner of a block with the force location represented by vector r. The torque cross product equation gives the resulting torque in all 3 axes.

For more information on the cross product, see https://en.wikipedia.org/wiki/Cross_product The Vector class has a static function for calculating the cross product which the simulation code below will use.

Moment of Inertia


https://en.wikipedia.org/wiki/Moment_of_inertia

All physical objects have a mass, and all physical objects have a moment of inertia. Where mass is an object's resistance to movement, moment of inertia is an object's resistance to rotation.

And where mass is the same from any orientation, the moment of inertia can vary from orientation. There are simple formulas to calculate moment of inertia for a variety of shapes:

Uniform Solid Sphere (moment of inertia same from any orientation):




Block (moment of inertia varies by axis):





https://en.wikipedia.org/wiki/List_of_moments_of_inertia

What happens if you have a more complex shape? (Two spheres at the end of a long block.) Or you're rotating somewhere other than the axis that the moment of inertia equations were referenced from? (Rotating from the end of the block rather than the center.) In those situations, you use the Parallel-Axis Theorem to recalculate a new moment of inertia.

https://en.wikipedia.org/wiki/Parallel_axis_theorem

Currently in the Orbital Aero Model, all bodies are single uniform spheres. I have plans to make it so bodies can consist of one of a variety of basic shapes or a combination of many basic shapes to form complex shapes. So I'll hold off on covering the parallel axis theorem until I complete that code.

Angular Acceleration & Velocity


https://en.wikipedia.org/wiki/Kinematics

In our prior function calls, we calculated the total moment applied to the body. Since we know its moment of inertia, it's easy to calculate acceleration:


T is the torque, in newton-meters.
I is the moment of inertia, in kilogram meters squared.
alpha is the angular acceleration, in radians per second squared.

And with the angular acceleration, we can rotate the object:


theta is the newly calculated orientation, in radians.
theta0 is the initial orientation, in radians.
omega0 is the initial angular velocity, in radians per second.
alpha is the angular acceleration, in radians per second squared.
t is the time step of the simulation, in seconds.


omega is the newly calculated angular velocity, in radians per second.
omega0 is the initial angular velocity, in radians per second.
alpha is the angular acceleration, in radians per second squared.
t is the time step of the simulation, in seconds.

The smaller the time step the more accurate the simulation. However, that also means it's more computational intensive for simulating the same duration of time.



Orbital Aero Code


Now that all the background math has been covered, let's see how it's represented in code.

Body


I added a moment of inertia property to the Body class. Since everything in the orbital aero are considered uniform spheres for now, this returns a moment of inertia based on the body's mass and radius.


class Body
{
public:
    Vector momentOfInertiaTotal_kilogramMeters2() const;
.
.
.
}

Vector Body::momentOfInertiaTotal_kilogramMeters2() const
{
    // For solid spheres: I = 2/5 * M * R^2
    double momentOfInertia_kilogramMeters2 = (double)2.0 / (double)5.0 * this->massTotal_kilograms() * (this->radius_meters * this->radius_meters);
    return Vector(momentOfInertia_kilogramMeters2, momentOfInertia_kilogramMeters2, momentOfInertia_kilogramMeters2);
}

Control System


I'll demonstrate 2 ways of calculating and applying a moment to a body.

Reaction Wheel / Directly Calculate Moment from Joystick Deflection


This approach would be more for satellites with reaction wheels. Rather than expelling force via a rocket to rotate the vehicle, an electric motor attached to a flywheel spins causing the vehicle to counter-rotate. https://en.wikipedia.org/wiki/Reaction_wheel

Of course we could go in depth with the physics of the motor and flywheel, but for simplicity we can just scale the joystick input to a torque. And we can use the body's moment of inertia as a general rule for how much to scale the joystick input.


bool ControlSystemModel::update(OrbitalAeroModel &hostOrbitalAero, Body* hostBody, double timeStep_seconds)
{
  switch (controlInput)
  {
  case ControlInputEnum::idle:
  // No change to any angular velocities set.
  break;
  case ControlInputEnum::joystick:
  {
            // Scale the moment applied by the moment of inertia so our joystick inputs are effective for both
            // very small and very large bodies. This will result in 1 radians/second^2 for full joystick deflection.
            Vector momentOfIneretiaTotal_kilogramMeters2 = hostBody->momentOfInertiaTotal_kilogramMeters2();
            hostBody->momentTotal_newtonMeters.x = hostOrbitalAero.joystickInputs[this->joystickInputsIndex].roll * momentOfIneretiaTotal_kilogramMeters2.x;
            hostBody->momentTotal_newtonMeters.y = -hostOrbitalAero.joystickInputs[this->joystickInputsIndex].pitch * momentOfIneretiaTotal_kilogramMeters2.y;
            hostBody->momentTotal_newtonMeters.z = hostOrbitalAero.joystickInputs[this->joystickInputsIndex].yaw * momentOfIneretiaTotal_kilogramMeters2.z;

            // All thrust is along the x (forward) axis of the entity.
            double thrust_newtons = hostOrbitalAero.joystickInputs[this->joystickInputsIndex].thrust * 1000.0;

            // 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());
      }
         break;
  default:
  {
            this->hostBody->momentTotal_newtonMeters = 0.0;
            this->hostBody->forceThrust_newtons = 0.0;
      }
  break;
  }

  return true;
}

Orientation Thrusters / Calculate Moment from Rockets at Edges of Body


This approach is more inline with the Space Shuttle's reaction control system. https://en.wikipedia.org/wiki/Reaction_control_system A combination of small thrusters on various parts of the spacecraft are fired for attitude adjustments. (They could also make translation adjustments if opposing thrusters are not fired to counteract the force applied.)

The code below models 6 thrusters for orientation control. There is a pair for each axis; one on either side of the body. For any thrust created on one side, a thrust in the opposite direction is created on the other side so that the body only rotates (no translation).

These thrusters pretend they can create force in either direction, which in reality you wouldn't do. (Rocket nozzles point in one direction, not two.) Really this is 12 thrusters with math simplified for 6. That's not many though, the Space Shuttle had 44 of these! (But those were also used for minor translation changes and as backups in case of malfunction.)


bool ControlSystemModel::update(OrbitalAeroModel &hostOrbitalAero, Body* hostBody, double timeStep_seconds)
{
  switch (controlInput)
  {
  case ControlInputEnum::idle:
  // No change to any angular velocities set.
  break;
  case ControlInputEnum::joystick:
  {
             // Orientation Thrusters

            // Roll 1 Thruster - Right Wingtip, pointed up/down.
            Vector roll1ThrustersForce_newtons(0.0, 0.0, hostOrbitalAero.joystickInputs[this->joystickInputsIndex].roll * 10.0);
            Vector roll1ThrustersLocation_meters(0.0, hostBody->radius_meters, 0.0);
            Vector roll1ThrustersMoment_newtonMeters = Vector::crossProduct(roll1ThrustersLocation_meters, roll1ThrustersForce_newtons);

            // Roll 2 Thruster - Left Wingtip, pointed up/down.
            Vector roll2ThrustersForce_newtons(0.0, 0.0, hostOrbitalAero.joystickInputs[this->joystickInputsIndex].roll * -10.0);
            Vector roll2ThrustersLocation_meters(0.0, -hostBody->radius_meters, 0.0);
            Vector roll2ThrustersMoment_newtonMeters = Vector::crossProduct(roll2ThrustersLocation_meters, roll2ThrustersForce_newtons);

            // Pitch 1 Thruster - Nose, pointed up/down.
            Vector pitch1ThrustersForce_newtons(0.0, 0.0, hostOrbitalAero.joystickInputs[this->joystickInputsIndex].pitch * 10.0);
            Vector pitch1ThrustersLocation_meters(-hostBody->radius_meters, 0.0, 0.0);
            Vector pitch1ThrustersMoment_newtonMeters = Vector::crossProduct(pitch1ThrustersLocation_meters, pitch1ThrustersForce_newtons);

            // Pitch 2 Thruster - Tail, pointed up/down.
            Vector pitch2ThrustersForce_newtons(0.0, 0.0, hostOrbitalAero.joystickInputs[this->joystickInputsIndex].pitch * -10.0);
            Vector pitch2ThrustersLocation_meters(hostBody->radius_meters, 0.0, 0.0);
            Vector pitch2ThrustersMoment_newtonMeters = Vector::crossProduct(pitch2ThrustersLocation_meters, pitch2ThrustersForce_newtons);

            // Yaw 1 Thruster, Nose, pointed left/right.
            Vector yaw1ThrustersForce_newtons(0.0, hostOrbitalAero.joystickInputs[this->joystickInputsIndex].yaw * 10.0, 0.0);
            Vector yaw1ThrustersLocation_meters(hostBody->radius_meters, 0.0, 0.0);
            Vector yaw1ThrustersMoment_newtonMeters = Vector::crossProduct(yaw1ThrustersLocation_meters, yaw1ThrustersForce_newtons);

            // Yaw 2 Thruster, Tail, pointed left/right.
            Vector yaw2ThrustersForce_newtons(0.0, hostOrbitalAero.joystickInputs[this->joystickInputsIndex].yaw * -10.0, 0.0);
            Vector yaw2ThrustersLocation_meters(-hostBody->radius_meters, 0.0, 0.0);
            Vector yaw2ThrustersMoment_newtonMeters = Vector::crossProduct(yaw2ThrustersLocation_meters, yaw2ThrustersForce_newtons);

            // Main thruster is at the tail along x (forward) axis of the entity.
            Vector mainThrustersForce_newtons(hostOrbitalAero.joystickInputs[this->joystickInputsIndex].thrust * 1000.0, 0.0, 0.0);
            Vector mainThrustersLocation_meters(-hostBody->radius_meters, 0.0, 0.0);
            Vector mainThrustersMoment_newtonMeters = Vector::crossProduct(mainThrustersLocation_meters, mainThrustersForce_newtons);

            // Get total thrust.
            Vector totalThrustBody_newtons = roll1ThrustersForce_newtons + roll2ThrustersForce_newtons + 
                                             pitch1ThrustersForce_newtons + pitch2ThrustersForce_newtons + 
                                             yaw1ThrustersForce_newtons + yaw2ThrustersForce_newtons + 
                                             mainThrustersForce_newtons;

            // Get total moment.
            hostBody->momentTotal_newtonMeters = roll1ThrustersMoment_newtonMeters + roll2ThrustersMoment_newtonMeters + 
                                                 pitch1ThrustersMoment_newtonMeters + pitch2ThrustersMoment_newtonMeters + 
                                                 yaw1ThrustersMoment_newtonMeters + yaw2ThrustersMoment_newtonMeters + 
                                                 mainThrustersMoment_newtonMeters;

            // Convert thrust from local axis to world axis.
            hostBody->forceThrust_newtons = totalThrustBody_newtons.rotatedBy(hostBody->orientation_quaternions.inverse());

         break;
  default:
  {
            this->hostBody->momentTotal_newtonMeters = 0.0;
            this->hostBody->forceThrust_newtons = 0.0;
      }
  break;
  }


  return true;
}

processKinematics()


Kinematics have been updated to calculate the angular acceleration from the applied moment and moment of inertia.


//  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_t, Body*>::iterator iteratorPrimaryBody = tableBodies.begin();
  while (iteratorPrimaryBody != tableBodies.end())
  {
  Body* primaryBody = iteratorPrimaryBody->second;
  if (primaryBody->isActive && !primaryBody->isExternal)
  {
  if (primaryBody->isStationary)
  {
                // Body Stationary, No Movement
primaryBody->linearAcceleration_metersPerSecond2 = 0.0;
primaryBody->linearVelocity_metersPerSecond = 0.0;
                primaryBody->angularAcceleration_radiansPerSecond2.clear();
                primaryBody->angularVelocity_radiansPerSecond.clear();
  }
  else
  {
                // Translation
                primaryBody->linearAcceleration_metersPerSecond2 = primaryBody->forceTotal_newtons() / primaryBody->massTotal_kilograms();
                // Distance Travelled = (Initial Linear Velocity * Time) + (0.5 * Linear Acceleration * Time^2)
                primaryBody->location_meters += (primaryBody->linearVelocity_metersPerSecond * timeStep_seconds) + (primaryBody->linearAcceleration_metersPerSecond2 * (0.5 * timeStep_seconds * timeStep_seconds));
                // Final Linear Velocity = Initial Linear Velocity + (Linear Acceleration * Time)
primaryBody->linearVelocity_metersPerSecond += primaryBody->linearAcceleration_metersPerSecond2 * timeStep_seconds;
                
                // Rotation
                primaryBody->angularAcceleration_radiansPerSecond2 = primaryBody->momentTotal_newtonMeters / primaryBody->momentOfInertiaTotal_kilogramMeters2();
                // Rotation Amount = (Initial Angular Velocity * Time) + (0.5 * Angular Acceleration * Time^2)
                Euler rotationAmount_radians = (primaryBody->angularVelocity_radiansPerSecond * timeStep_seconds) + (primaryBody->angularAcceleration_radiansPerSecond2 * (0.5 * timeStep_seconds * timeStep_seconds));
                if (rotationAmount_radians.isNonZero())
                {
                    // Rotate around local axis.
                    primaryBody->orientation_quaternions.rotateAboutLocalAxis(rotationAmount_radians);
                }
                // Final Angular Velocity = Initial Angular Velocity + (Angular Acceleration * Time)
                primaryBody->angularVelocity_radiansPerSecond += primaryBody->angularAcceleration_radiansPerSecond2 * timeStep_seconds;
            }
  } // (primaryBody.isActive)
  iteratorPrimaryBody++;
  } // (iteratorPrimaryBody != tableBodies.end())

  return true;
}


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.