From: curt Date: Sat, 1 Dec 2001 06:22:24 +0000 (+0000) Subject: Initial revision of Andy Ross's YASim code. This is (Y)et (A)nother Flight X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=5b84ae51a54afb63effb8841ed08643bb5701aa7;p=flightgear.git Initial revision of Andy Ross's YASim code. This is (Y)et (A)nother Flight Dynamics (Sim)ulator. Basically, this is a rough, first cut of a "different take" on FDM design. It's intended to be very simple to use, producing reasonable results for aircraft of all sorts and sizes, while maintaining simulation plausibility even in odd flight conditions like spins and aerobatics. It's at the point now where one can actually fly the planes around. --- diff --git a/src/FDM/Makefile.am b/src/FDM/Makefile.am index c711e0b85..696f8d72e 100644 --- a/src/FDM/Makefile.am +++ b/src/FDM/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Balloon JSBSim LaRCsim UIUCModel +SUBDIRS = Balloon JSBSim LaRCsim UIUCModel YASim noinst_LIBRARIES = libFlight.a diff --git a/src/FDM/YASim/Airplane.cpp b/src/FDM/YASim/Airplane.cpp new file mode 100644 index 000000000..93a822a3e --- /dev/null +++ b/src/FDM/YASim/Airplane.cpp @@ -0,0 +1,799 @@ +#include "Atmosphere.hpp" +#include "ControlMap.hpp" +#include "Gear.hpp" +#include "Math.hpp" +#include "Glue.hpp" +#include "RigidBody.hpp" +#include "Surface.hpp" +#include "Thruster.hpp" + +#include "Airplane.hpp" +namespace yasim { + +// FIXME: hook gear extension into the force calculation somehow... + +Airplane::Airplane() +{ + _emptyWeight = 0; + _pilotPos[0] = _pilotPos[1] = _pilotPos[2] = 0; + _wing = 0; + _tail = 0; + _ballast = 0; + _cruiseRho = 0; + _cruiseSpeed = 0; + _cruiseWeight = 0; + _approachRho = 0; + _approachSpeed = 0; + _approachAoA = 0; + _approachWeight = 0; + + _dragFactor = 1; + _liftRatio = 1; + _cruiseAoA = 0; + _tailIncidence = 0; +} + +Airplane::~Airplane() +{ + for(int i=0; i<_fuselages.size(); i++) + delete (Fuselage*)_fuselages.get(i); + for(int i=0; i<_tanks.size(); i++) + delete (Tank*)_tanks.get(i); + for(int i=0; i<_thrusters.size(); i++) + delete (ThrustRec*)_thrusters.get(i); + for(int i=0; i<_gears.size(); i++) + delete (GearRec*)_gears.get(i); + for(int i=0; i<_surfs.size(); i++) + delete (Surface*)_surfs.get(i); +} + +void Airplane::iterate(float dt) +{ + _model.iterate(); + + // Consume fuel + // FIXME +} + +ControlMap* Airplane::getControlMap() +{ + return &_controls; +} + +Model* Airplane::getModel() +{ + return &_model; +} + +void Airplane::getPilotAccel(float* out) +{ + State* s = _model.getState(); + + // Gravity + Glue::geodUp(s->pos, out); + Math::mul3(-9.8, out, out); + + // The regular acceleration + float tmp[3]; + Math::mul3(-1, s->acc, tmp); + Math::add3(tmp, out, out); + + // Convert to aircraft coordinates + Math::vmul33(s->orient, out, out); + + // FIXME: rotational & centripetal acceleration needed +} + +void Airplane::setPilotPos(float* pos) +{ + for(int i=0; i<3; i++) _pilotPos[i] = pos[i]; +} + +void Airplane::getPilotPos(float* out) +{ + for(int i=0; i<3; i++) out[i] = _pilotPos[i]; +} + +int Airplane::numGear() +{ + return _gears.size(); +} + +Gear* Airplane::getGear(int g) +{ + return ((GearRec*)_gears.get(g))->gear; +} + +void Airplane::setGearState(bool down, float dt) +{ + for(int i=0; i<_gears.size(); i++) { + GearRec* gr = (GearRec*)_gears.get(i); + if(gr->time == 0) { + // Non-extensible + gr->gear->setExtension(1); + gr->surf->setXDrag(1); + gr->surf->setYDrag(1); + gr->surf->setZDrag(1); + continue; + } + + float diff = dt / gr->time; + if(!down) diff = -diff; + float ext = gr->gear->getExtension() + diff; + if(ext < 0) ext = 0; + if(ext > 1) ext = 1; + + gr->gear->setExtension(ext); + gr->surf->setXDrag(ext); + gr->surf->setYDrag(ext); + gr->surf->setZDrag(ext); + } +} + +void Airplane::setApproach(float speed, float altitude) +{ + // The zero AoA will become a calculated stall AoA in compile() + setApproach(speed, altitude, 0); +} + +void Airplane::setApproach(float speed, float altitude, float aoa) +{ + _approachSpeed = speed; + _approachRho = Atmosphere::getStdDensity(altitude); + _approachAoA = aoa; +} + +void Airplane::setCruise(float speed, float altitude) +{ + _cruiseSpeed = speed; + _cruiseRho = Atmosphere::getStdDensity(altitude); + _cruiseAoA = 0; + _tailIncidence = 0; +} + +void Airplane::addApproachControl(int control, float val) +{ + Control* c = new Control(); + c->control = control; + c->val = val; + _approachControls.add(c); +} + +void Airplane::addCruiseControl(int control, float val) +{ + Control* c = new Control(); + c->control = control; + c->val = val; + _cruiseControls.add(c); +} + +int Airplane::numTanks() +{ + return _tanks.size(); +} + +float Airplane::getFuel(int tank) +{ + return ((Tank*)_tanks.get(tank))->fill; +} + +float Airplane::getFuelDensity(int tank) +{ + return ((Tank*)_tanks.get(tank))->density; +} + +void Airplane::setWeight(float weight) +{ + _emptyWeight = weight; +} + +void Airplane::setWing(Wing* wing) +{ + _wing = wing; +} + +void Airplane::setTail(Wing* tail) +{ + _tail = tail; +} + +void Airplane::addVStab(Wing* vstab) +{ + _vstabs.add(vstab); +} + +void Airplane::addFuselage(float* front, float* back, float width) +{ + Fuselage* f = new Fuselage(); + for(int i=0; i<3; i++) { + f->front[i] = front[i]; + f->back[i] = back[i]; + } + f->width = width; + _fuselages.add(f); +} + +int Airplane::addTank(float* pos, float cap, float density) +{ + Tank* t = new Tank(); + for(int i=0; i<3; i++) t->pos[i] = pos[i]; + t->cap = cap; + t->fill = cap; + t->density = density; + t->handle = 0xffffffff; + return _tanks.add(t); +} + +void Airplane::addGear(Gear* gear, float transitionTime) +{ + GearRec* g = new GearRec(); + g->gear = gear; + g->surf = 0; + g->time = transitionTime; + _gears.add(g); +} + +void Airplane::addThruster(Thruster* thruster, float mass, float* cg) +{ + ThrustRec* t = new ThrustRec(); + t->thruster = thruster; + t->mass = mass; + for(int i=0; i<3; i++) t->cg[i] = cg[i]; + _thrusters.add(t); +} + +void Airplane::addBallast(float* pos, float mass) +{ + _model.getBody()->addMass(mass, pos); + _ballast += mass; +} + +int Airplane::addWeight(float* pos, float size) +{ + WeightRec* wr = new WeightRec(); + wr->handle = _model.getBody()->addMass(0, pos); + + wr->surf = new Surface(); + wr->surf->setTotalDrag(size*size); + _model.addSurface(wr->surf); + _surfs.add(wr->surf); + + return _weights.add(wr); +} + +void Airplane::setWeight(int handle, float mass) +{ + WeightRec* wr = (WeightRec*)_weights.get(handle); + + _model.getBody()->setMass(wr->handle, mass); + + // Kill the aerodynamic drag if the mass is exactly zero. This is + // how we simulate droppable stores. + if(mass == 0) { + wr->surf->setXDrag(0); + wr->surf->setYDrag(0); + wr->surf->setZDrag(0); + } else { + wr->surf->setXDrag(1); + wr->surf->setYDrag(1); + wr->surf->setZDrag(1); + } +} + +void Airplane::setFuelFraction(float frac) +{ + for(int i=0; i<_tanks.size(); i++) { + Tank* t = (Tank*)_tanks.get(i); + _model.getBody()->setMass(t->handle, t->cap * frac); + } +} + +float Airplane::getDragCoefficient() +{ + return _dragFactor; +} + +float Airplane::getLiftRatio() +{ + return _liftRatio; +} + +float Airplane::getCruiseAoA() +{ + return _cruiseAoA; +} + +float Airplane::getTailIncidence() +{ + return _tailIncidence; +} + +char* Airplane::getFailureMsg() +{ + return _failureMsg; +} + +int Airplane::getSolutionIterations() +{ + return _solutionIterations; +} + +void Airplane::setupState(float aoa, float speed, State* s) +{ + float cosAoA = Math::cos(aoa); + float sinAoA = Math::sin(aoa); + s->orient[0] = cosAoA; s->orient[1] = 0; s->orient[2] = sinAoA; + s->orient[3] = 0; s->orient[4] = 1; s->orient[5] = 0; + s->orient[6] = -sinAoA; s->orient[7] = 0; s->orient[8] = cosAoA; + + s->v[0] = speed; s->v[1] = 0; s->v[2] = 0; + + for(int i=0; i<3; i++) + s->pos[i] = s->rot[i] = s->acc[i] = s->racc[i] = 0; + + // Put us 1m above the origin, or else the gravity computation in + // Model goes nuts + s->pos[2] = 1; +} + +float Airplane::compileWing(Wing* w) +{ + // Make sure it's initialized. The surfaces will pop out with + // total drag coefficients equal to their areas, which is what we + // want. + w->compile(); + + float wgt = 0; + for(int i=0; inumSurfaces(); i++) { + Surface* s = (Surface*)w->getSurface(i); + _model.addSurface(s); + + float pos[3]; + s->getPosition(pos); + _model.getBody()->addMass(w->getSurfaceWeight(i), pos); + wgt += w->getSurfaceWeight(i); + } + return wgt; +} + +float Airplane::compileFuselage(Fuselage* f) +{ + float wgt = 0; + float fwd[3]; + Math::sub3(f->front, f->back, fwd); + float len = Math::mag3(fwd); + float wid = f->width; + int segs = (int)Math::ceil(len/wid); + float segWgt = wid*wid/segs; + for(int j=0; jback, pos, pos); + _model.getBody()->addMass(segWgt, pos); + wgt += segWgt; + + // Make a Surface too + Surface* s = new Surface(); + s->setPosition(pos); + float sideDrag = len/wid; + s->setYDrag(sideDrag); + s->setZDrag(sideDrag); + s->setTotalDrag(segWgt); + + // FIXME: fails for fuselages aligned along the Y axis + float o[9]; + float *x=o, *y=o+3, *z=o+6; // nicknames for the axes + Math::unit3(fwd, x); + y[0] = 0; y[1] = 1; y[2] = 0; + Math::cross3(x, y, z); + s->setOrientation(o); + + _model.addSurface(s); + _surfs.add(s); + } + return wgt; +} + +// FIXME: should probably add a mass for the gear, too +void Airplane::compileGear(GearRec* gr) +{ + Gear* g = gr->gear; + + // Make a Surface object for the aerodynamic behavior + Surface* s = new Surface(); + gr->surf = s; + + // Put the surface at the half-way point on the gear strut, give + // it a drag coefficient equal to a square of the same dimension + // (gear are really draggy) and make it symmetric. + float pos[3], cmp[3]; + g->getCompression(cmp); + float length = Math::mag3(cmp); + g->getPosition(pos); + Math::mul3(0.5, cmp, cmp); + Math::add3(pos, cmp, pos); + + s->setPosition(pos); + s->setTotalDrag(length*length); + + _model.addGear(g); + _model.addSurface(s); + _surfs.add(s); +} + +void Airplane::compile() +{ + double ground[3]; + ground[0] = 0; ground[1] = 0; ground[2] = 1; + _model.setGroundPlane(ground, -100000); + + RigidBody* body = _model.getBody(); + int firstMass = body->numMasses(); + + // Generate the point masses for the plane. Just use unitless + // numbers for a first pass, then go back through and rescale to + // make the weight right. + float aeroWgt = 0; + + // The Wing objects + aeroWgt += compileWing(_wing); + aeroWgt += compileWing(_tail); + for(int i=0; i<_vstabs.size(); i++) { + aeroWgt += compileWing((Wing*)_vstabs.get(i)); + } + + // The fuselage(s) + for(int i=0; i<_fuselages.size(); i++) { + aeroWgt += compileFuselage((Fuselage*)_fuselages.get(i)); + } + + // Count up the absolute weight we have + float nonAeroWgt = _ballast; + for(int i=0; i<_thrusters.size(); i++) + nonAeroWgt += ((ThrustRec*)_thrusters.get(i))->mass; + + // Rescale to the specified empty weight + float wscale = (_emptyWeight-nonAeroWgt)/aeroWgt; + for(int i=firstMass; inumMasses(); i++) + body->setMass(i, body->getMass(i)*wscale); + + // Add the thruster masses + for(int i=0; i<_thrusters.size(); i++) { + ThrustRec* t = (ThrustRec*)_thrusters.get(i); + body->addMass(t->mass, t->cg); + } + + // Add the tanks, empty for now. + float totalFuel = 0; + for(int i=0; i<_tanks.size(); i++) { + Tank* t = (Tank*)_tanks.get(i); + t->handle = body->addMass(0, t->pos); + totalFuel += t->cap; + } + _cruiseWeight = _emptyWeight + totalFuel*0.5; + _approachWeight = _emptyWeight + totalFuel*0.2; + + body->recalc(); + + // Add surfaces for the landing gear. + for(int i=0; i<_gears.size(); i++) + compileGear((GearRec*)_gears.get(i)); + + // The Thruster objects + for(int i=0; i<_thrusters.size(); i++) { + ThrustRec* tr = (ThrustRec*)_thrusters.get(i); + tr->handle = _model.addThruster(tr->thruster); + } + + solveGear(); + solve(); + + // Drop the gear (use a really big dt) + setGearState(true, 1000000); +} + +void Airplane::solveGear() +{ + float cg[3], pos[3]; + _model.getBody()->getCG(cg); + + // Calculate spring constant weightings for the gear. Weight by + // the inverse of the distance to the c.g. in the XY plane, which + // should be correct for most gear arrangements. Add 50cm of + // "buffer" to keep things from blowing up with aircraft with a + // single gear very near the c.g. (AV-8, for example). + float total = 0; + for(int i=0; i<_gears.size(); i++) { + GearRec* gr = (GearRec*)_gears.get(i); + Gear* g = gr->gear; + g->getPosition(pos); + Math::sub3(cg, pos, pos); + gr->wgt = 1/(0.5+Math::sqrt(pos[0]*pos[0] + pos[1]*pos[1])); + total += gr->wgt; + } + + // Renormalize so they sum to 1 + for(int i=0; i<_gears.size(); i++) + ((GearRec*)_gears.get(i))->wgt /= total; + + // The force at max compression should be sufficient to stop a + // plane moving downwards at 3x the approach descent rate. Assume + // a 3 degree approach. + float descentRate = 3*_approachSpeed/19.1; + + // Spread the kinetic energy according to the gear weights. This + // will results in an equal compression fraction (not distance) of + // each gear. + float energy = 0.5*_approachWeight*descentRate*descentRate; + + for(int i=0; i<_gears.size(); i++) { + GearRec* gr = (GearRec*)_gears.get(i); + float e = energy * gr->wgt; + float comp[3]; + gr->gear->getCompression(comp); + float len = Math::mag3(comp); + + // Energy in a spring: e = 0.5 * k * len^2 + float k = 2 * e / (len*len); + + gr->gear->setSpring(k); + + // Critically damped (too damped, too!) + gr->gear->setDamping(2*Math::sqrt(k*_approachWeight*gr->wgt)); + + // These are pretty generic + gr->gear->setStaticFriction(0.8); + gr->gear->setDynamicFriction(0.7); + } +} + +void Airplane::stabilizeThrust() +{ + float thrust[3], tmp[3]; + float last = 0; + while(1) { + thrust[0] = thrust[1] = thrust[2] = 0; + for(int i=0; i<_thrusters.size(); i++) { + Thruster* t = _model.getThruster(i); + t->integrate(0.033); + t->getThrust(tmp); + Math::add3(thrust, tmp, thrust); + } + + float t = Math::mag3(thrust); + float ratio = (t+0.1)/(last+0.1); + if(ratio < 1.00001 && ratio > 0.99999) + break; + + last = t; + } +} + +void Airplane::runCruise() +{ + setupState(_cruiseAoA, _cruiseSpeed, &_cruiseState); + _model.setState(&_cruiseState); + _model.setAirDensity(_cruiseRho); + + // The control configuration + _controls.reset(); + for(int i=0; i<_cruiseControls.size(); i++) { + Control* c = (Control*)_cruiseControls.get(i); + _controls.setInput(c->control, c->val); + } + _controls.applyControls(); + + // The local wind + float wind[3]; + Math::mul3(-1, _cruiseState.v, wind); + Math::vmul33(_cruiseState.orient, wind, wind); + + // Gear are up (if they're non-retractable, this is a noop) + setGearState(false, 100000); + + // Cruise is by convention at 50% tank capacity + setFuelFraction(0.5); + + // Set up the thruster parameters and iterate until the thrust + // stabilizes. + for(int i=0; i<_thrusters.size(); i++) { + Thruster* t = ((ThrustRec*)_thrusters.get(i))->thruster; + t->setWind(wind); + t->setDensity(_cruiseRho); + } + stabilizeThrust(); + + // Precompute thrust in the model, and calculate aerodynamic forces + _model.getBody()->reset(); + _model.initIteration(); + _model.calcForces(&_cruiseState); +} + +void Airplane::runApproach() +{ + setupState(_approachAoA, _approachSpeed, &_approachState); + _model.setState(&_approachState); + _model.setAirDensity(_approachRho); + + // The control configuration + _controls.reset(); + for(int i=0; i<_approachControls.size(); i++) { + Control* c = (Control*)_approachControls.get(i); + _controls.setInput(c->control, c->val); + } + _controls.applyControls(); + + // The local wind + float wind[3]; + Math::mul3(-1, _approachState.v, wind); + Math::vmul33(_approachState.orient, wind, wind); + + // Approach is by convention at 20% tank capacity + setFuelFraction(0.2); + + // Gear are down + setGearState(true, 100000); + + // Run the thrusters until they get to a stable setting. FIXME: + // this is lots of wasted work. + for(int i=0; i<_thrusters.size(); i++) { + Thruster* t = ((ThrustRec*)_thrusters.get(i))->thruster; + t->setWind(wind); + t->setDensity(_approachRho); + } + stabilizeThrust(); + + // Precompute thrust in the model, and calculate aerodynamic forces + _model.getBody()->reset(); + _model.initIteration(); + _model.calcForces(&_approachState); +} + +void Airplane::applyDragFactor(float factor) +{ + float applied = Math::sqrt(factor); + _dragFactor *= applied; + _wing->setDragScale(_wing->getDragScale() * applied); + _tail->setDragScale(_tail->getDragScale() * applied); + for(int i=0; i<_vstabs.size(); i++) { + Wing* w = (Wing*)_vstabs.get(i); + w->setDragScale(w->getDragScale() * applied); + } + for(int i=0; i<_surfs.size(); i++) { + Surface* s = (Surface*)_surfs.get(i); + s->setTotalDrag(s->getTotalDrag() * applied); + } +} + +void Airplane::applyLiftRatio(float factor) +{ + float applied = Math::sqrt(factor); + _liftRatio *= applied; + _wing->setLiftRatio(_wing->getLiftRatio() * applied); + _tail->setLiftRatio(_tail->getLiftRatio() * applied); + for(int i=0; i<_vstabs.size(); i++) { + Wing* w = (Wing*)_vstabs.get(i); + w->setLiftRatio(w->getLiftRatio() * applied); + } +} + +float Airplane::clamp(float val, float min, float max) +{ + if(val < min) return min; + if(val > max) return max; + return val; +} + +float Airplane::normFactor(float f) +{ + if(f < 0) f = -f; + if(f < 1) f = 1/f; + return f; +} + +void Airplane::solve() +{ + static const float ARCMIN = 0.0002909; + + float tmp[3]; + _solutionIterations = 0; + _failureMsg = 0; + while(1) { + if(_solutionIterations++ > 10000) { + _failureMsg = "Solution failed to converge after 10000 iterations"; + return; + } + + // Run an iteration at cruise, and extract the needed numbers: + runCruise(); + + _model.getThrust(tmp); + float thrust = tmp[0]; + + _model.getBody()->getAccel(tmp); + float xforce = _cruiseWeight * tmp[0]; + float clift0 = _cruiseWeight * tmp[2]; + + _model.getBody()->getAngularAccel(tmp); + float pitch0 = tmp[1]; + + // Run an approach iteration, and do likewise + runApproach(); + + _model.getBody()->getAccel(tmp); + float alift = _approachWeight * tmp[2]; + + // Modify the cruise AoA a bit to get a derivative + _cruiseAoA += ARCMIN; + runCruise(); + _cruiseAoA -= ARCMIN; + + _model.getBody()->getAccel(tmp); + float clift1 = _cruiseWeight * tmp[2]; + + // Do the same with the tail incidence + _tail->setIncidence(_tailIncidence + ARCMIN); + runCruise(); + _tail->setIncidence(_tailIncidence); + + _model.getBody()->getAngularAccel(tmp); + float pitch1 = tmp[1]; + + // Now calculate: + float awgt = 9.8 * _approachWeight; + + float dragFactor = thrust / (thrust-xforce); + float liftFactor = awgt / (awgt+alift); + float aoaDelta = -clift0 * (ARCMIN/(clift1-clift0)); + float tailDelta = -pitch0 * (ARCMIN/(pitch1-pitch0)); + + // Sanity: + if(dragFactor <= 0) { + _failureMsg = "Zero or negative drag adjustment."; + return; + } else if(liftFactor <= 0) { + _failureMsg = "Zero or negative lift adjustment."; + return; + } + + // And apply: + applyDragFactor(dragFactor); + applyLiftRatio(liftFactor); + + // DON'T do the following until the above are sane + if(normFactor(dragFactor) > 1.1 + || normFactor(liftFactor) > 1.1) + { + continue; + } + + // OK, now we can adjust the minor variables + _cruiseAoA += 0.5*aoaDelta; + _tailIncidence += 0.5*tailDelta; + + _cruiseAoA = clamp(_cruiseAoA, -.174, .174); + _tailIncidence = clamp(_tailIncidence, -.174, .174); + + if(dragFactor < 1.00001 && liftFactor < 1.00001 && + aoaDelta < .000017 && tailDelta < .000017) + { + break; + } + } + + if(_dragFactor < 1e-06 || _dragFactor > 1e6) { + _failureMsg = "Drag factor beyond reasonable bounds."; + return; + } else if(_liftRatio < 1e-04 || _liftRatio > 1e4) { + _failureMsg = "Lift ratio beyond reasonable bounds."; + return; + } else if(Math::abs(_cruiseAoA) >= .174) { + _failureMsg = "Cruise AoA > 10 degrees"; + return; + } else if(Math::abs(_tailIncidence) >= .174) { + _failureMsg = "Tail incidence > 10 degrees"; + return; + } +} +}; // namespace yasim diff --git a/src/FDM/YASim/Airplane.hpp b/src/FDM/YASim/Airplane.hpp new file mode 100644 index 000000000..250d1e15e --- /dev/null +++ b/src/FDM/YASim/Airplane.hpp @@ -0,0 +1,135 @@ +#ifndef _AIRPLANE_HPP +#define _AIRPLANE_HPP + +#include "ControlMap.hpp" +#include "Model.hpp" +#include "Wing.hpp" +#include "util/Vector.hpp" + +namespace yasim { + +class Gear; +class Thruster; + +class Airplane { +public: + Airplane(); + ~Airplane(); + + void iterate(float dt); + + ControlMap* getControlMap(); + Model* getModel(); + + void setPilotPos(float* pos); + void getPilotPos(float* out); + + void getPilotAccel(float* out); + + void setWeight(float weight); + + void setWing(Wing* wing); + void setTail(Wing* tail); + void addVStab(Wing* vstab); + + void addFuselage(float* front, float* back, float width); + int addTank(float* pos, float cap, float fuelDensity); + void addGear(Gear* g, float transitionTime); + void addThruster(Thruster* t, float mass, float* cg); + void addBallast(float* pos, float mass); + + int addWeight(float* pos, float size); + void setWeight(int handle, float mass); + + void setApproach(float speed, float altitude); + void setApproach(float speed, float altitude, float aoa); + void setCruise(float speed, float altitude); + + void addApproachControl(int control, float val); + void addCruiseControl(int control, float val); + + int numGear(); + Gear* getGear(int g); + void setGearState(bool down, float dt); + + int numTanks(); + void setFuelFraction(float frac); // 0-1, total amount of fuel + float getFuel(int tank); // in kg! + float getFuelDensity(int tank); // kg/m^3 + + void compile(); // generate point masses & such, then solve + + // Solution output values + int getSolutionIterations(); + float getDragCoefficient(); + float getLiftRatio(); + float getCruiseAoA(); + float getTailIncidence(); + char* getFailureMsg(); + +private: + struct Tank { float pos[3]; float cap; float fill; + float density; int handle; }; + struct Fuselage { float front[3], back[3], width; }; + struct GearRec { Gear* gear; Surface* surf; float wgt; float time; }; + struct ThrustRec { Thruster* thruster; + int handle; float cg[3]; float mass; }; + struct Control { int control; float val; }; + struct WeightRec { int handle; Surface* surf; }; + + void runCruise(); + void runApproach(); + void setupState(float aoa, float speed, State* s); + void solveGear(); + void solve(); + float compileWing(Wing* w); + float compileFuselage(Fuselage* f); + void compileGear(GearRec* gr); + void stabilizeThrust(); + void applyDragFactor(float factor); + void applyLiftRatio(float factor); + float clamp(float val, float min, float max); + float normFactor(float f); + + Model _model; + ControlMap _controls; + + float _emptyWeight; + float _pilotPos[3]; + + Wing* _wing; + Wing* _tail; + + Vector _fuselages; + Vector _vstabs; + Vector _tanks; + Vector _thrusters; + float _ballast; + + Vector _gears; + Vector _weights; + Vector _surfs; // NON-wing Surfaces + + Vector _cruiseControls; + State _cruiseState; + float _cruiseRho; + float _cruiseSpeed; + float _cruiseWeight; + + Vector _approachControls; + State _approachState; + float _approachRho; + float _approachSpeed; + float _approachAoA; + float _approachWeight; + + int _solutionIterations; + float _dragFactor; + float _liftRatio; + float _cruiseAoA; + float _tailIncidence; + char* _failureMsg; +}; + +}; // namespace yasim +#endif // _AIRPLANE_HPP diff --git a/src/FDM/YASim/Atmosphere.cpp b/src/FDM/YASim/Atmosphere.cpp new file mode 100644 index 000000000..c331b2faf --- /dev/null +++ b/src/FDM/YASim/Atmosphere.cpp @@ -0,0 +1,120 @@ +#include "Math.hpp" +#include "Atmosphere.hpp" +namespace yasim { + +// Copied from McCormick, who got it from "The ARDC Model Atmosphere" +// Note that there's an error in the text in the first entry, +// McCormick lists 299.16/101325/1.22500, but those don't agree with +// R=287. I chose to correct the temperature to 288.20, since 79F is +// pretty hot for a "standard" atmosphere. +// meters kelvin kPa kg/m^3 +float Atmosphere::data[][4] = {{ 0, 288.20, 101325, 1.22500 }, + { 900, 282.31, 90971, 1.12260 }, + { 1800, 276.46, 81494, 1.02690 }, + { 2700, 270.62, 72835, 0.93765 }, + { 3600, 264.77, 64939, 0.85445 }, + { 4500, 258.93, 57752, 0.77704 }, + { 5400, 253.09, 51226, 0.70513 }, + { 6300, 247.25, 45311, 0.63845 }, + { 7200, 241.41, 39963, 0.57671 }, + { 8100, 235.58, 35140, 0.51967 }, + { 9000, 229.74, 30800, 0.46706 }, + { 9900, 223.91, 26906, 0.41864 }, + { 10800, 218.08, 23422, 0.37417 }, + { 11700, 216.66, 20335, 0.32699 }, + { 12600, 216.66, 17654, 0.28388 }, + { 13500, 216.66, 15327, 0.24646 }, + { 14400, 216.66, 13308, 0.21399 }, + { 15300, 216.66, 11555, 0.18580 }, + { 16200, 216.66, 10033, 0.16133 }, + { 17100, 216.66, 8712, 0.14009 }, + { 18000, 216.66, 7565, 0.12165 }, + { 18900, 216.66, 6570, 0.10564 }}; + +float Atmosphere::getStdTemperature(float alt) +{ + return getRecord(alt, 1); +} + +float Atmosphere::getStdPressure(float alt) +{ + return getRecord(alt, 2); +} + +float Atmosphere::getStdDensity(float alt) +{ + return getRecord(alt, 3); +} + +float Atmosphere::calcVEAS(float spd, float pressure, float temp) +{ + return 0; //FIXME +} + +float Atmosphere::calcVCAS(float spd, float pressure, float temp) +{ + // Stolen shamelessly from JSBSim. Constants that appear: + // 2/5 == gamma-1 + // 5/12 == 1/(gamma+1) + // 4/5 == 2*(gamma-1) + // 14/5 == 2*gamma + // 28/5 == 4*gamma + // 144/25 == (gamma+1)^2 + + float m2 = calcMach(spd, temp); + m2 = m2*m2; // mach^2 + + float cp; // pressure coefficient + if(m2 < 1) { + // (1+(mach^2)/5)^(gamma/(gamma-1)) + cp = Math::pow(1+0.2*m2, 3.5); + } else { + float tmp0 = ((144/25.) * m2) / (28/5.*m2 - 4/5.); + float tmp1 = ((14/5.) * m2 - (2/5.)) * (5/12.); + cp = Math::pow(tmp0, 3.5) * tmp1; + } + + // Conditions at sea level + float p0 = getStdPressure(0); + float rho0 = getStdDensity(0); + + float tmp = Math::pow((pressure/p0)*(cp-1) + 1, (2/7.)); + return Math::sqrt((7*p0/rho0)*(tmp-1)); +} + +float Atmosphere::calcDensity(float pressure, float temp) +{ + // P = rho*R*T, R == 287 kPa*m^3 per kg*kelvin for air + return pressure / (287 * temp); +} + +float Atmosphere::calcMach(float spd, float temp) +{ + return spd / Math::sqrt(1.4 * 287 * temp); +} + +float Atmosphere::getRecord(float alt, int recNum) +{ + int hi = (sizeof(data) / (4*sizeof(float))) - 1; + int lo = 0; + + // safety valve, clamp to the edges of the table + if(alt < data[0][0]) hi=1; + else if(alt > data[hi][0]) lo = hi-1; + + // binary search + while(1) { + if(hi-lo == 1) break; + int mid = (hi+lo)>>1; + if(alt < data[mid][0]) hi = mid; + else lo = mid; + } + + // interpolate + float frac = (alt - data[lo][0])/(data[hi][0] - data[lo][0]); + float a = data[lo][recNum]; + float b = data[hi][recNum]; + return a + frac * (b-a); +} + +}; // namespace yasim diff --git a/src/FDM/YASim/Atmosphere.hpp b/src/FDM/YASim/Atmosphere.hpp new file mode 100644 index 000000000..2e9c0512f --- /dev/null +++ b/src/FDM/YASim/Atmosphere.hpp @@ -0,0 +1,23 @@ +#ifndef _ATMOSPHERE_HPP +#define _ATMOSPHERE_HPP + +namespace yasim { + +class Atmosphere { +public: + static float getStdTemperature(float alt); + static float getStdPressure(float alt); + static float getStdDensity(float alt); + + static float calcVCAS(float spd, float pressure, float temp); + static float calcVEAS(float spd, float pressure, float temp); + static float calcMach(float spd, float temp); + static float calcDensity(float pressure, float temp); + +private: + static float getRecord(float alt, int idx); + static float data[][4]; +}; + +}; // namespace yasim +#endif // _ATMOSPHERE_HPP diff --git a/src/FDM/YASim/BodyEnvironment.hpp b/src/FDM/YASim/BodyEnvironment.hpp new file mode 100644 index 000000000..3d6d38efc --- /dev/null +++ b/src/FDM/YASim/BodyEnvironment.hpp @@ -0,0 +1,59 @@ +#ifndef _BODYENVIRONMENT_HPP +#define _BODYENVIRONMENT_HPP + +#include "RigidBody.hpp" + +namespace yasim { + +// The values for position and orientation within the global reference +// frame, along with their first and second time derivatives. The +// orientation is stored as a matrix for simplicity. Note that +// because it is orthonormal, its inverse is simply its transpose. +// You can get local->global transformations by calling Math::tmul33() +// and using the same matrix. +struct State { + double pos[3]; // position + float orient[9]; // global->local xform matrix + float v[3]; // velocity + float rot[3]; // rotational velocity + float acc[3]; // acceleration + float racc[3]; // rotational acceleration + + // Simple initialization + State() { + for(int i=0; i<3; i++) { + pos[i] = v[i] = rot[i] = acc[i] = racc[i] = 0; + for(int j=0; j<3; j++) + orient[3*i+j] = i==j ? 1 : 0; + } + } +}; + +// +// Objects implementing this interface are responsible for calculating +// external forces on a RigidBody object. These will then be used by +// an Integrator to decide on a new solution to the state equations, +// which will be reported to the BodyEnvironment for further action. +// +class BodyEnvironment +{ +public: + // This method inspects the "environment" in which a RigidBody + // exists, calculates forces and torques on the body, and sets + // them via addForce()/addTorque(). Because this method is called + // multiple times ("trials") as part of a Runge-Kutta integration, + // this is NOT the place to make decisions about anything but the + // forces on the object. Note that the acc and racc fields of the + // passed-in State object are undefined! (They are calculed BY + // this method). + virtual void calcForces(State* state) = 0; + + // Called when the RK4 integrator has determined a "real" new + // point on the curve of life. Any side-effect producing checks + // of body state vs. the environment can happen here (crashes, + // etc...). + virtual void newState(State* state) = 0; +}; + +}; // namespace yasim +#endif // _BODYENVIRONMENT_HPP diff --git a/src/FDM/YASim/ControlMap.cpp b/src/FDM/YASim/ControlMap.cpp new file mode 100644 index 000000000..e738b5b08 --- /dev/null +++ b/src/FDM/YASim/ControlMap.cpp @@ -0,0 +1,130 @@ +#include "Jet.hpp" +#include "Thruster.hpp" +#include "Gear.hpp" +#include "Wing.hpp" +#include "Math.hpp" + +#include "ControlMap.hpp" +namespace yasim { + +ControlMap::~ControlMap() +{ + for(int i=0; i<_inputs.size(); i++) { + Vector* v = (Vector*)_inputs.get(i); + for(int j=0; jsize(); j++) + delete (MapRec*)v->get(j); + delete v; + } + + for(int i=0; i<_outputs.size(); i++) { + OutRec* o = (OutRec*)_outputs.get(i); + delete[] o->values; + delete o; + } +} + +int ControlMap::newInput() +{ + Vector* v = new Vector(); + return _inputs.add(v); +} + +void ControlMap::addMapping(int input, int type, void* object, int options) +{ + // See if the output object already exists + OutRec* out = 0; + for(int i=0; i<_outputs.size(); i++) { + OutRec* o = (OutRec*)_outputs.get(i); + if(o->object == object && o->type == type) { + out = o; + break; + } + } + + // Create one if it doesn't + if(out == 0) { + out = new OutRec(); + out->type = type; + out->object = object; + out->n = 0; + out->values = 0; + _outputs.add(out); + } + + // Make space for the new input value + int idx = out->n++; + delete[] out->values; + out->values = new float[out->n]; + + // Add the new option tag + out->options.add((void*)options); + + // Make a new input record + MapRec* map = new MapRec(); + map->out = out; + map->idx = idx; + + // And add it to the approproate vector. + Vector* maps = (Vector*)_inputs.get(input); + maps->add(map); +} + +void ControlMap::reset() +{ + // Set all the values to zero + for(int i=0; i<_outputs.size(); i++) { + OutRec* o = (OutRec*)_outputs.get(i); + for(int j=0; jn; j++) + o->values[j] = 0; + } +} + +void ControlMap::setInput(int input, float value) +{ + Vector* maps = (Vector*)_inputs.get(input); + for(int i=0; isize(); i++) { + MapRec* map = (MapRec*)maps->get(i); + map->out->values[map->idx] = value; + } +} + +void ControlMap::applyControls() +{ + for(int outrec=0; outrec<_outputs.size(); outrec++) { + OutRec* o = (OutRec*)_outputs.get(outrec); + + // Generate a summed value. Note the check for "split" + // control axes like ailerons. + float lval = 0, rval = 0; + for(int i=0; in; i++) { + float val = o->values[i]; + int opt = (int)o->options.get(i); + if(opt & OPT_SQUARE) + val = val * Math::abs(val); + if(opt & OPT_INVERT) + val = -val; + lval += val; + if(opt & OPT_SPLIT) + rval -= val; + else + rval += val; + } + + void* obj = o->object; + switch(o->type) { + case THROTTLE: ((Thruster*)obj)->setThrottle(lval); break; + case MIXTURE: ((Thruster*)obj)->setMixture(lval); break; + case PROP: ((Thruster*)obj)->setPropAdvance(lval); break; + case REHEAT: ((Jet*)obj)->setReheat(lval); break; + case BRAKE: ((Gear*)obj)->setBrake(lval); break; + case STEER: ((Gear*)obj)->setRotation(lval); break; + case EXTEND: ((Gear*)obj)->setExtension(lval); break; + case SLAT: ((Wing*)obj)->setSlat(lval); break; + case FLAP0: ((Wing*)obj)->setFlap0(lval, rval); break; + case FLAP1: ((Wing*)obj)->setFlap1(lval, rval); break; + case SPOILER: ((Wing*)obj)->setSpoiler(lval, rval); break; + } + } +} + +}; // namespace yasim diff --git a/src/FDM/YASim/ControlMap.hpp b/src/FDM/YASim/ControlMap.hpp new file mode 100644 index 000000000..0534298cf --- /dev/null +++ b/src/FDM/YASim/ControlMap.hpp @@ -0,0 +1,54 @@ +#ifndef _CONTROL_MAP_HPP +#define _CONTROL_MAP_HPP + +#include "util/Vector.hpp" + +namespace yasim { + +class ControlMap { +public: + ~ControlMap(); + + enum OutputType { THROTTLE, MIXTURE, REHEAT, PROP, + BRAKE, STEER, EXTEND, + INCIDENCE, FLAP0, FLAP1, SLAT, SPOILER }; + + static const int OPT_SPLIT = 0x01; + static const int OPT_INVERT = 0x02; + static const int OPT_SQUARE = 0x04; + + // Returns a new, not-yet-used "input handle" for addMapping and + // setInput. This typically corresponds to one user axis. + int newInput(); + + // Adds a mapping to between input handle and a particular setting + // on an output object. The value of output MUST match the type + // of object! + void addMapping(int input, int output, void* object, int options=0); + + // Resets our accumulated input values. Call before any + // setInput() invokations. + void reset(); + + // Sets the specified input (as returned by newInput) to the + // specified value. + void setInput(int input, float value); + + // Calculates and applies the settings received since the last reset(). + void applyControls(); + +private: + struct OutRec { int type; void* object; int n; + float* values; Vector options; }; + struct MapRec { OutRec* out; int idx; }; + + // A list of (sub)Vectors containing a bunch of MapRec objects for + // each input handle. + Vector _inputs; + + // An unordered list of output settings. + Vector _outputs; +}; + +}; // namespace yasim +#endif // _CONTROL_MAP_HPP diff --git a/src/FDM/YASim/FGFDM.cpp b/src/FDM/YASim/FGFDM.cpp new file mode 100644 index 000000000..f606b15fa --- /dev/null +++ b/src/FDM/YASim/FGFDM.cpp @@ -0,0 +1,458 @@ +#include +#include + +#include
+ +#include "Jet.hpp" +#include "Gear.hpp" +#include "Atmosphere.hpp" +#include "PropEngine.hpp" +#include "Propeller.hpp" +#include "PistonEngine.hpp" + +#include "FGFDM.hpp" +namespace yasim { + +// Some conversion factors +static const float KTS2MPS = 0.514444444444; +static const float FT2M = 0.3048; +static const float DEG2RAD = 0.0174532925199; +static const float RPM2RAD = 0.10471975512; +static const float LBS2N = 4.44822; +static const float LBS2KG = 0.45359237; +static const float CM2GALS = 264.172037284; +static const float HP2W = 745.700; + +// Stubs, so that this can be compiled without the FlightGear +// binary. What's the best way to handle this? + +// float fgGetFloat(char* name, float def) { return 0; } +// void fgSetFloat(char* name, float val) {} + +FGFDM::FGFDM() +{ + _nextEngine = 0; +} + +FGFDM::~FGFDM() +{ + for(int i=0; i<_axes.size(); i++) { + AxisRec* a = (AxisRec*)_axes.get(i); + delete[] a->name; + delete a; + } + for(int i=0; i<_pistons.size(); i++) { + EngRec* er = (EngRec*)_pistons.get(i); + delete[] er->prefix; + delete (PropEngine*)er->eng; + delete er; + } + for(int i=0; i<_jets.size(); i++) { + EngRec* er = (EngRec*)_pistons.get(i); + delete[] er->prefix; + delete (Jet*)er->eng; + delete er; + } + for(int i=0; i<_weights.size(); i++) { + WeightRec* wr = (WeightRec*)_weights.get(i); + delete[] wr->prop; + delete wr; + } + +} + +void FGFDM::iterate(float dt) +{ + getExternalInput(dt); + _airplane.iterate(dt); + setOutputProperties(); +} + +Airplane* FGFDM::getAirplane() +{ + return &_airplane; +} + +void FGFDM::init() +{ + // We don't want to use these ties (we set the values ourselves, + // and this works only for the first piston engine anyway). + fgUntie("/engines/engine[0]/rpm"); + fgUntie("/engines/engine[0]/egt-degf"); + fgUntie("/engines/engine[0]/cht-degf"); + fgUntie("/engines/engine[0]/oil-temperature-degf"); + fgUntie("/engines/engine[0]/mp-osi"); + fgUntie("/engines/engine[0]/fuel-flow-gph"); + fgUntie("/engines/engine[0]/running"); + fgUntie("/engines/engine[0]/cranking"); + fgUntie("/consumables/fuel/tank[0]/level-gal_us"); + fgUntie("/consumables/fuel/tank[1]/level-gal_us"); + + // Set these to sane values. We don't support engine start yet. + fgSetBool("/engines/engine[0]/running", true); + fgSetBool("/engines/engine[0]/cranking", false); + + // Allows the user to start with something other than full fuel + _airplane.setFuelFraction(fgGetFloat("/yasim/fuel-fraction", 1)); + + // This has a nasty habit of being false at startup. That's not + // good. + fgSetBool("/controls/gear-down", true); +} + +// Not the worlds safest parser. But it's short & sweet. +void FGFDM::startElement(const char* name, const XMLAttributes &atts) +{ + XMLAttributes* a = (XMLAttributes*)&atts; + float v[3]; + char buf[64]; + + if(eq(name, "airplane")) { + _airplane.setWeight(attrf(a, "mass") * LBS2KG); + } else if(eq(name, "approach")) { + float spd = attrf(a, "speed") * KTS2MPS; + float alt = attrf(a, "alt", 0) * FT2M; + float aoa = attrf(a, "aoa", 0) * DEG2RAD; + _airplane.setApproach(spd, alt, aoa); + _cruiseCurr = false; + } else if(eq(name, "cruise")) { + float spd = attrf(a, "speed") * KTS2MPS; + float alt = attrf(a, "alt") * FT2M; + _airplane.setCruise(spd, alt); + _cruiseCurr = true; + } else if(eq(name, "cockpit")) { + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + _airplane.setPilotPos(v); + } else if(eq(name, "wing")) { + _airplane.setWing(parseWing(a, name)); + } else if(eq(name, "hstab")) { + _airplane.setTail(parseWing(a, name)); + } else if(eq(name, "vstab")) { + _airplane.addVStab(parseWing(a, name)); + } else if(eq(name, "propeller")) { + parsePropeller(a); + } else if(eq(name, "jet")) { + Jet* j = new Jet(); + _currObj = j; + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + float mass = attrf(a, "mass") * LBS2KG; + j->setDryThrust(attrf(a, "thrust") * LBS2N); + j->setPosition(v); + _airplane.addThruster(j, mass, v); + sprintf(buf, "/engines/engine[%d]", _nextEngine++); + EngRec* er = new EngRec(); + er->eng = j; + er->prefix = dup(buf); + _jets.add(er); + } else if(eq(name, "gear")) { + Gear* g = new Gear(); + _currObj = g; + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + g->setPosition(v); + v[0] = 0; + v[1] = 0; + v[2] = attrf(a, "compression", 1); + g->setCompression(v); + g->setStaticFriction(attrf(a, "sfric", 0.8)); + g->setDynamicFriction(attrf(a, "dfric", 0.7)); + float transitionTime = attrf(a, "retract-time", 0); + _airplane.addGear(g, transitionTime); + } else if(eq(name, "fuselage")) { + float b[3]; + v[0] = attrf(a, "ax"); + v[1] = attrf(a, "ay"); + v[2] = attrf(a, "az"); + b[0] = attrf(a, "bx"); + b[1] = attrf(a, "by"); + b[2] = attrf(a, "bz"); + _airplane.addFuselage(v, b, attrf(a, "width")); + } else if(eq(name, "tank")) { + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + float density = 6.0; // gasoline, in lbs/gal + if(a->hasAttribute("jet")) density = 6.72; + density *= LBS2KG/CM2GALS; + _airplane.addTank(v, attrf(a, "capacity") * LBS2KG, density); + } else if(eq(name, "ballast")) { + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + _airplane.addBallast(v, attrf(a, "mass") * LBS2KG); + } else if(eq(name, "weight")) { + parseWeight(a); + } else if(eq(name, "stall")) { + Wing* w = (Wing*)_currObj; + w->setStall(attrf(a, "aoa") * DEG2RAD); + w->setStallWidth(attrf(a, "width", 2) * DEG2RAD); + w->setStallPeak(attrf(a, "peak", 1.5)); + } else if(eq(name, "flap0")) { + ((Wing*)_currObj)->setFlap0(attrf(a, "start"), attrf(a, "end"), + attrf(a, "lift"), attrf(a, "drag")); + } else if(eq(name, "flap1")) { + ((Wing*)_currObj)->setFlap1(attrf(a, "start"), attrf(a, "end"), + attrf(a, "lift"), attrf(a, "drag")); + } else if(eq(name, "slat")) { + ((Wing*)_currObj)->setSlat(attrf(a, "start"), attrf(a, "end"), + attrf(a, "aoa"), attrf(a, "drag")); + } else if(eq(name, "spoiler")) { + ((Wing*)_currObj)->setSpoiler(attrf(a, "start"), attrf(a, "end"), + attrf(a, "lift"), attrf(a, "drag")); + } else if(eq(name, "actionpt")) { + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + ((Thruster*)_currObj)->setPosition(v); + } else if(eq(name, "dir")) { + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + ((Thruster*)_currObj)->setDirection(v); + } else if(eq(name, "control")) { + const char* axis = a->getValue("axis"); + if(a->hasAttribute("output")) { + // assert: output type must match _currObj type! + const char* output = a->getValue("output"); + int opt = 0; + opt |= a->hasAttribute("split") ? ControlMap::OPT_SPLIT : 0; + opt |= a->hasAttribute("invert") ? ControlMap::OPT_INVERT : 0; + opt |= a->hasAttribute("square") ? ControlMap::OPT_SQUARE : 0; + _airplane.getControlMap()->addMapping(parseAxis(axis), + parseOutput(output), + _currObj, + opt); + } else { + // assert: must be under a "cruise" or "approach" tag + float value = attrf(a, "value", 0); + if(_cruiseCurr) + _airplane.addCruiseControl(parseAxis(axis), value); + else + _airplane.addApproachControl(parseAxis(axis), value); + } + } else { + *(int*)0=0; // unexpected tag, boom + } +} + +void FGFDM::getExternalInput(float dt) +{ + // The control axes + ControlMap* cm = _airplane.getControlMap(); + cm->reset(); + for(int i=0; i<_axes.size(); i++) { + AxisRec* a = (AxisRec*)_axes.get(i); + float val = fgGetFloat(a->name, 0); + cm->setInput(a->handle, val); + } + cm->applyControls(); + + // Weights + for(int i=0; i<_weights.size(); i++) { + WeightRec* wr = (WeightRec*)_weights.get(i); + _airplane.setWeight(wr->handle, fgGetFloat(wr->prop)); + } + + // Gear state + _airplane.setGearState(fgGetBool("/controls/gear-down"), dt); +} + +void FGFDM::setOutputProperties() +{ + char buf[256]; + for(int i=0; i<_airplane.numTanks(); i++) { + sprintf(buf, "/consumables/fuel/tank[%d]/level-gal_us", i); + fgSetFloat(buf, + CM2GALS*_airplane.getFuel(i)/_airplane.getFuelDensity(i)); + } + + for(int i=0; i<_pistons.size(); i++) { + EngRec* er = (EngRec*)_pistons.get(i); + PropEngine* p = (PropEngine*)er->eng; + + sprintf(buf, "%s/rpm", er->prefix); + fgSetFloat(buf, p->getOmega() * (30/3.15149265358979)); + + sprintf(buf, "%s/fuel-flow-gph", er->prefix); + fgSetFloat(buf, p->getFuelFlow() * (3600*2.2/5)); // FIXME, wrong + } + + for(int i=0; i<_jets.size(); i++) { + EngRec* er = (EngRec*)_jets.get(i); + Jet* j = (Jet*)er->eng; + + sprintf(buf, "%s/fuel-flow-gph", er->prefix); + fgSetFloat(buf, j->getFuelFlow() * (3600*2.2/6)); // FIXME, wrong + } +} + +Wing* FGFDM::parseWing(XMLAttributes* a, const char* type) +{ + Wing* w = new Wing(); + + float defDihed = 0; + if(eq(type, "vstab")) + defDihed = 90; + else + w->setMirror(true); + + float pos[3]; + pos[0] = attrf(a, "x"); + pos[1] = attrf(a, "y"); + pos[2] = attrf(a, "z"); + w->setBase(pos); + + w->setLength(attrf(a, "length")); + w->setChord(attrf(a, "chord")); + w->setSweep(attrf(a, "sweep", 0) * DEG2RAD); + w->setTaper(attrf(a, "taper", 1)); + w->setDihedral(attrf(a, "dihedral", defDihed) * DEG2RAD); + w->setCamber(attrf(a, "camber", 0)); + w->setIncidence(attrf(a, "incidence", 0) * DEG2RAD); + + float effect = attrf(a, "effectiveness", 1); + w->setDragScale(w->getDragScale()*effect); + + _currObj = w; + return w; +} + +void FGFDM::parsePropeller(XMLAttributes* a) +{ + float cg[3]; + cg[0] = attrf(a, "x"); + cg[1] = attrf(a, "y"); + cg[2] = attrf(a, "z"); + float mass = attrf(a, "mass") * LBS2KG; + float moment = attrf(a, "moment"); + float radius = attrf(a, "radius"); + float speed = attrf(a, "cruise-speed") * KTS2MPS; + float omega = attrf(a, "cruise-rpm") * RPM2RAD; + float rho = Atmosphere::getStdDensity(attrf(a, "alt") * FT2M); + float power = attrf(a, "takeoff-power") * HP2W; + float omega0 = attrf(a, "takeoff-rpm") * RPM2RAD; + + // FIXME: this is a hack. Find a better way to ask the engine how + // much power it can produce under cruise conditions. + float cruisePower = (power * (rho/Atmosphere::getStdDensity(0)) + * (omega/omega0)); + + Propeller* prop = new Propeller(radius, speed, omega, rho, cruisePower, + omega0, power); + PistonEngine* eng = new PistonEngine(power, omega0); + PropEngine* thruster = new PropEngine(prop, eng, moment); + _airplane.addThruster(thruster, mass, cg); + + char buf[64]; + sprintf(buf, "/engines/engine[%d]", _nextEngine++); + EngRec* er = new EngRec(); + er->eng = thruster; + er->prefix = dup(buf); + _pistons.add(er); + + _currObj = thruster; +} + +// Turns a string axis name into an integer for use by the +// ControlMap. Creates a new axis if this one hasn't been defined +// yet. +int FGFDM::parseAxis(const char* name) +{ + for(int i=0; i<_axes.size(); i++) { + AxisRec* a = (AxisRec*)_axes.get(i); + if(eq(a->name, name)) + return a->handle; + } + + // Not there, make a new one. + AxisRec* a = new AxisRec(); + a->name = dup(name); + a->handle = _airplane.getControlMap()->newInput(); + _axes.add(a); + return a->handle; +} + +int FGFDM::parseOutput(const char* name) +{ + if(eq(name, "THROTTLE")) return ControlMap::THROTTLE; + if(eq(name, "MIXTURE")) return ControlMap::MIXTURE; + if(eq(name, "REHEAT")) return ControlMap::REHEAT; + if(eq(name, "PROP")) return ControlMap::PROP; + if(eq(name, "BRAKE")) return ControlMap::BRAKE; + if(eq(name, "STEER")) return ControlMap::STEER; + if(eq(name, "EXTEND")) return ControlMap::EXTEND; + if(eq(name, "INCIDENCE")) return ControlMap::INCIDENCE; + if(eq(name, "FLAP0")) return ControlMap::FLAP0; + if(eq(name, "FLAP1")) return ControlMap::FLAP1; + if(eq(name, "SLAT")) return ControlMap::SLAT; + if(eq(name, "SPOILER")) return ControlMap::SPOILER; + // error here... + return -1; +} + +void FGFDM::parseWeight(XMLAttributes* a) +{ + WeightRec* wr = new WeightRec(); + + float v[3]; + v[0] = attrf(a, "x"); + v[1] = attrf(a, "y"); + v[2] = attrf(a, "z"); + + wr->prop = dup(a->getValue("mass-prop")); + wr->size = attrf(a, "size", 0); + wr->handle = _airplane.addWeight(v, wr->size); + + _weights.add(wr); +} + +bool FGFDM::eq(const char* a, const char* b) +{ + // Figure it out for yourself. :) + while(*a && *b && *a++ == *b++); + return !(*a || *b); +} + +char* FGFDM::dup(const char* s) +{ + int len=0; + while(s[len++]); + char* s2 = new char[len+1]; + char* p = s2; + while((*p++ = *s++)); + s2[len] = 0; + return s2; +} + +int FGFDM::attri(XMLAttributes* atts, char* attr) +{ + if(!atts->hasAttribute(attr)) *(int*)0=0; // boom + return attri(atts, attr, 0); +} + +int FGFDM::attri(XMLAttributes* atts, char* attr, int def) +{ + const char* val = atts->getValue(attr); + if(val == 0) return def; + else return atol(val); +} + +float FGFDM::attrf(XMLAttributes* atts, char* attr) +{ + if(!atts->hasAttribute(attr)) *(int*)0=0; // boom + return attrf(atts, attr, 0); +} + +float FGFDM::attrf(XMLAttributes* atts, char* attr, float def) +{ + const char* val = atts->getValue(attr); + if(val == 0) return def; + else return (float)atof(val); +} + +}; // namespace yasim diff --git a/src/FDM/YASim/FGFDM.hpp b/src/FDM/YASim/FGFDM.hpp new file mode 100644 index 000000000..1df02b653 --- /dev/null +++ b/src/FDM/YASim/FGFDM.hpp @@ -0,0 +1,69 @@ +#ifndef _FGFDM_HPP +#define _FGFDM_HPP + +#include + +#include "Airplane.hpp" +#include "util/Vector.hpp" + +namespace yasim { + +class Wing; + +// This class forms the "glue" to the FlightGear codebase. It handles +// parsing of XML airplane files, interfacing to the properties +// system, and providing data for the use of the FGInterface object. +class FGFDM : public XMLVisitor { +public: + FGFDM(); + ~FGFDM(); + void init(); + void iterate(float dt); + + Airplane* getAirplane(); + + // XML parsing callback from XMLVisitor + virtual void startElement(const char* name, const XMLAttributes &atts); + +private: + struct AxisRec { char* name; int handle; }; + struct EngRec { char* prefix; void* eng; }; + struct WeightRec { char* prop; float size; int handle; }; + + void getExternalInput(float dt); + void setOutputProperties(); + + Wing* parseWing(XMLAttributes* a, const char* name); + int parseAxis(const char* name); + int parseOutput(const char* name); + void parseWeight(XMLAttributes* a); + void parsePropeller(XMLAttributes* a); + bool eq(const char* a, const char* b); + char* dup(const char* s); + int attri(XMLAttributes* atts, char* attr); + int attri(XMLAttributes* atts, char* attr, int def); + float attrf(XMLAttributes* atts, char* attr); + float attrf(XMLAttributes* atts, char* attr, float def); + + // The core Airplane object we manage. + Airplane _airplane; + + // The list of "axes" that we expect to find as input. These are + // typically property names. + Vector _axes; + + // Settable weights + Vector _weights; + + // Engine types. Contains an EngRec structure. + Vector _jets; + Vector _pistons; + + // Parsing temporaries + void* _currObj; + bool _cruiseCurr; + int _nextEngine; +}; + +}; // namespace yasim +#endif // _FGFDM_HPP diff --git a/src/FDM/YASim/Gear.cpp b/src/FDM/YASim/Gear.cpp new file mode 100644 index 000000000..056bc61a4 --- /dev/null +++ b/src/FDM/YASim/Gear.cpp @@ -0,0 +1,240 @@ +#include "Math.hpp" +#include "RigidBody.hpp" + +#include "Gear.hpp" +namespace yasim { + +Gear::Gear() +{ + for(int i=0; i<3; i++) + _pos[i] = _cmpr[i] = 0; + _spring = 1; + _damp = 0; + _sfric = 0.8; + _dfric = 0.7; + _brake = 0; + _rot = 0; + _extension = 1; +} + +void Gear::setPosition(float* position) +{ + for(int i=0; i<3; i++) _pos[i] = position[i]; +} + +void Gear::setCompression(float* compression) +{ + for(int i=0; i<3; i++) _cmpr[i] = compression[i]; +} + +void Gear::setSpring(float spring) +{ + _spring = spring; +} + +void Gear::setDamping(float damping) +{ + _damp = damping; +} + +void Gear::setStaticFriction(float sfric) +{ + _sfric = sfric; +} + +void Gear::setDynamicFriction(float dfric) +{ + _dfric = dfric; +} + +void Gear::setBrake(float brake) +{ + _brake = brake; +} + +void Gear::setRotation(float rotation) +{ + _rot = rotation; +} + +void Gear::setExtension(float extension) +{ + _extension = extension; +} + +void Gear::getPosition(float* out) +{ + for(int i=0; i<3; i++) out[i] = _pos[i]; +} + +void Gear::getCompression(float* out) +{ + for(int i=0; i<3; i++) out[i] = _cmpr[i]; +} + +float Gear::getSpring() +{ + return _spring; +} + +float Gear::getDamping() +{ + return _damp; +} + +float Gear::getStaticFriction() +{ + return _sfric; +} + +float Gear::getDynamicFriction() +{ + return _dfric; +} + +float Gear::getBrake() +{ + return _brake; +} + +float Gear::getRotation() +{ + return _rot; +} + +float Gear::getExtension() +{ + return _extension; +} + +void Gear::getForce(float* force, float* contact) +{ + Math::set3(_force, force); + Math::set3(_contact, contact); +} + +float Gear::getWoW() +{ + return _wow; +} + +float Gear::getCompressFraction() +{ + return _frac; +} + +void Gear::calcForce(RigidBody* body, float* v, float* rot, float* ground) +{ + // Init the return values + for(int i=0; i<3; i++) _force[i] = _contact[i] = 0; + + // Don't bother if it's not down + if(_extension < 1) + return; + + float tmp[3]; + + // First off, make sure that the gear "tip" is below the ground. + // If it's not, there's no force. + float a = ground[3] - Math::dot3(_pos, ground); + if(a > 0) + return; + + // Now a is the distance from the tip to ground, so make b the + // distance from the base to ground. We can get the fraction + // (0-1) of compression from a/(a-b). Note the minus sign -- stuff + // above ground is negative. + Math::add3(_cmpr, _pos, tmp); + float b = ground[3] - Math::dot3(tmp, ground); + + // Calculate the point of ground _contact. + _frac = a/(a-b); + if(b < 0) _frac = 1; + for(int i=0; i<3; i++) + _contact[i] = _pos[i] + _frac*_cmpr[i]; + + // Turn _cmpr into a unit vector and a magnitude + float cmpr[3]; + float clen = Math::mag3(_cmpr); + Math::mul3(1/clen, _cmpr, cmpr); + + // Now get the velocity of the point of contact + float cv[3]; + body->pointVelocity(_contact, rot, cv); + Math::add3(cv, v, cv); + + // Finally, we can start adding up the forces. First the + // spring compression (note the clamping of _frac to 1): + _frac = (_frac > 1) ? 1 : _frac; + float fmag = _frac*clen*_spring; + Math::mul3(fmag, cmpr, _force); + + // Then the damping. Use the only the velocity into the ground + // (projection along "ground") projected along the compression + // axis. So Vdamp = ground*(ground dot cv) dot cmpr + Math::mul3(Math::dot3(ground, cv), ground, tmp); + float dv = Math::dot3(cmpr, tmp); + float damp = _damp * dv; + if(damp > fmag) damp = fmag; // can't pull the plane down! + if(damp < -fmag) damp = -fmag; // sanity + Math::mul3(-damp, cmpr, tmp); + Math::add3(_force, tmp, _force); + + _wow = fmag - damp; + + // Wheels are funky. Split the velocity along the ground plane + // into rolling and skidding components. Assuming small angles, + // we generate "forward" and "left" unit vectors (the compression + // goes "up") for the gear, make a "steer" direction from these, + // and then project it onto the ground plane. Project the + // velocity onto the ground plane too, and extract the "steer" + // component. The remainder is the skid velocity. + + float gup[3]; // "up" unit vector from the ground + Math::set3(ground, gup); + Math::mul3(-1, gup, gup); + + float xhat[] = {1,0,0}; + float steer[3], skid[3]; + Math::cross3(gup, xhat, skid); // up cross xhat =~ skid + Math::unit3(skid, skid); // == skid + + Math::cross3(skid, gup, steer); // skid cross up == steer + + if(_rot != 0) { + // Correct for a (small) rotation + Math::mul3(_rot, steer, tmp); + Math::add3(tmp, skid, skid); + Math::unit3(skid, skid); + Math::cross3(skid, gup, steer); + } + + float vsteer = Math::dot3(cv, steer); + float vskid = Math::dot3(cv, skid); + float wgt = Math::dot3(_force, gup); // force into the ground + + float fsteer = _brake * calcFriction(wgt, vsteer); + float fskid = calcFriction(wgt, vskid); + if(vsteer > 0) fsteer = -fsteer; + if(vskid > 0) fskid = -fskid; + + // Phoo! All done. Add it up and get out of here. + Math::mul3(fsteer, steer, tmp); + Math::add3(tmp, _force, _force); + + Math::mul3(fskid, skid, tmp); + Math::add3(tmp, _force, _force); +} + +float Gear::calcFriction(float wgt, float v) +{ + // How slow is stopped? 50 cm/second? + const float STOP = 0.5; + const float iSTOP = 1/STOP; + v = Math::abs(v); + if(v < STOP) return v*iSTOP * wgt * _sfric; + else return wgt * _dfric; +} + +}; // namespace yasim + diff --git a/src/FDM/YASim/Gear.hpp b/src/FDM/YASim/Gear.hpp new file mode 100644 index 000000000..6e432b6dd --- /dev/null +++ b/src/FDM/YASim/Gear.hpp @@ -0,0 +1,82 @@ +#ifndef _GEAR_HPP +#define _GEAR_HPP + +namespace yasim { + +class RigidBody; + +// A landing gear has the following parameters: +// +// position: a point in the aircraft's local coordinate system where the +// fully-extended wheel will be found. +// compression: a vector from position where a fully-compressed wheel +// will be, also in aircraft-local coordinates. +// spring coefficient: force coefficient along the compression axis, in +// Newtons per meter. +// damping coefficient: force per compression speed, in Ns/m +// static coefficient of friction: force along the ground plane exerted +// by a motionless wheel. A unitless scalar. +// dynamic coefficient of friction: force along the ground plane exerted +// by a sliding/skidding wheel. +// braking fraction: fraction of the dynamic friction that will be +// actually exerted by a rolling wheel +// rotation: the angle from "forward" by which the wheel is rotated +// around its compression axis. In radians. +// +class Gear { +public: + Gear(); + + // Externally set values + void setPosition(float* position); + void setCompression(float* compression); + void setSpring(float spring); + void setDamping(float damping); + void setStaticFriction(float sfric); + void setDynamicFriction(float dfric); + void setBrake(float brake); + void setRotation(float rotation); + void setExtension(float extension); + + void getPosition(float* out); + void getCompression(float* out); + float getSpring(); + float getDamping(); + float getStaticFriction(); + float getDynamicFriction(); + float getBrake(); + float getRotation(); + float getExtension(); + + // Takes a velocity of the aircraft relative to ground, a rotation + // vector, and a ground plane (all specified in local coordinates) + // and make a force and point of application (i.e. ground contact) + // available via getForce(). + void calcForce(RigidBody* body, float* v, float* rot, float* ground); + + // Computed values: total force, weight-on-wheels (force normal to + // ground) and compression fraction. + void getForce(float* force, float* contact); + float getWoW(); + float getCompressFraction(); + +private: + float calcFriction(float wgt, float v); + + float _pos[3]; + float _cmpr[3]; + float _spring; + float _damp; + float _sfric; + float _dfric; + float _brake; + float _rot; + float _extension; + float _force[3]; + float _contact[3]; + float _wow; + float _frac; +}; + +}; // namespace yasim +#endif // _GEAR_HPP diff --git a/src/FDM/YASim/Glue.cpp b/src/FDM/YASim/Glue.cpp new file mode 100644 index 000000000..119a1b161 --- /dev/null +++ b/src/FDM/YASim/Glue.cpp @@ -0,0 +1,235 @@ +#include "Math.hpp" +#include "Glue.hpp" +namespace yasim { + +void Glue::calcAlphaBeta(State* s, float* alpha, float* beta) +{ + // Convert the velocity to the aircraft frame. + float v[3]; + Math::vmul33(s->orient, s->v, v); + + // By convention, positive alpha is an up pitch, and a positive + // beta is yawed to the right. + *alpha = -Math::atan2(v[2], v[0]); + *beta = Math::atan2(v[1], v[0]); +} + +void Glue::calcEulerRates(State* s, float* roll, float* pitch, float* hdg) +{ + // This one is easy, the projection of the rotation vector around + // the "up" axis. + float up[3]; + geodUp(s->pos, up); + *hdg = -Math::dot3(up, s->rot); // negate for "NED" conventions + + // A bit harder: the X component of the rotation vector expressed + // in airframe coordinates. + float lr[3]; + Math::vmul33(s->orient, s->rot, lr); + *roll = lr[0]; + + // Hardest: the component of rotation along the direction formed + // by the cross product of (and thus perpendicular to) the + // aircraft's forward vector (i.e. the first three elements of the + // orientation matrix) and the "up" axis. + float pitchAxis[3]; + Math::cross3(s->orient, up, pitchAxis); + Math::unit3(pitchAxis, pitchAxis); + *pitch = Math::dot3(pitchAxis, s->rot); +} + +void Glue::xyz2geoc(double* xyz, double* lat, double* lon, double* alt) +{ + double x=xyz[0], y=xyz[1], z=xyz[2]; + + // Cylindrical radius from the polar axis + double rcyl = Math::sqrt(x*x + y*y); + + // In geocentric coordinates, these are just the angles in + // cartesian space. + *lon = Math::atan2(y, x); + *lat = Math::atan2(z, rcyl); + + // To get XYZ coordinate of "ground", we "squash" the cylindric + // radius into a coordinate system where the earth is a sphere, + // find the fraction of the xyz vector that is above ground. + double rsquash = SQUASH * rcyl; + double frac = POLRAD/Math::sqrt(rsquash*rsquash + z*z); + double len = Math::sqrt(x*x + y*y + z*z); + + *alt = len * (1-frac); +} + +void Glue::geoc2xyz(double lat, double lon, double alt, double* out) +{ + // Generate a unit vector + double rcyl = Math::cos(lat); + double x = rcyl*Math::cos(lon); + double y = rcyl*Math::sin(lon); + double z = Math::sin(lat); + + // Convert to "squashed" space, renormalize the unit vector, + // multiply by the polar radius, and back convert to get us the + // point of intersection of the unit vector with the surface. + // Then just add the altitude. + double rtmp = rcyl*SQUASH; + double renorm = POLRAD/Math::sqrt(rtmp*rtmp + z*z); + double ztmp = z*renorm; + rtmp *= renorm*STRETCH; + double len = Math::sqrt(rtmp*rtmp + ztmp*ztmp); + len += alt; + + out[0] = x*len; + out[1] = y*len; + out[2] = z*len; +} + +double Glue::geod2geocLat(double lat) +{ + double r = Math::cos(lat)*STRETCH*STRETCH; + double z = Math::sin(lat); + return Math::atan2(z, r); +} + +double Glue::geoc2geodLat(double lat) +{ + double r = Math::cos(lat)*SQUASH*SQUASH; + double z = Math::sin(lat); + return Math::atan2(z, r); +} + +void Glue::xyz2geod(double* xyz, double* lat, double* lon, double* alt) +{ + xyz2geoc(xyz, lat, lon, alt); + *lat = geoc2geodLat(*lat); +} + +void Glue::geod2xyz(double lat, double lon, double alt, double* out) +{ + lat = geod2geocLat(lat); + geoc2xyz(lat, lon, alt, out); +} + +void Glue::xyz2nedMat(double lat, double lon, float* out) +{ + // Shorthand for our output vectors: + float *north = out, *east = out+3, *down = out+6; + + float slat = Math::sin(lat); + float clat = Math::cos(lat); + float slon = Math::sin(lon); + float clon = Math::cos(lon); + + north[0] = -clon * slat; + north[1] = -slon * slat; + north[2] = clat; + + east[0] = -slon; + east[1] = clon; + east[2] = 0; + + down[0] = -clon * clat; + down[1] = -slon * clat; + down[2] = -slat; +} + +void Glue::euler2orient(float roll, float pitch, float hdg, float* out) +{ + // To translate a point in aircraft space to the output "NED" + // frame, first negate the Y and Z axes (ugh), then roll around + // the X axis, then pitch around Y, then point to the correct + // heading about Z. Expressed as a matrix multiplication, then, + // the transformation from aircraft to local is HPRN. And our + // desired output is the inverse (i.e. transpose) of that. Since + // all rotations are 2D, they have a simpler form than a generic + // rotation and are done out longhand below for efficiency. + + // Init to the identity matrix + for(int i=0; i<3; i++) + for(int j=0; j<3; j++) + out[3*i+j] = (i==j) ? 1 : 0; + + // Negate Y and Z + out[4] = out[8] = -1; + + float s = Math::sin(roll); + float c = Math::cos(roll); + for(int col=0; col<3; col++) { + float y=out[col+3], z=out[col+6]; + out[col+3] = c*y - s*z; + out[col+6] = s*y + c*z; + } + + s = Math::sin(pitch); + c = Math::cos(pitch); + for(int col=0; col<3; col++) { + float x=out[col], z=out[col+6]; + out[col] = c*x + s*z; + out[col+6] = c*z - s*x; + } + + s = Math::sin(hdg); + c = Math::cos(hdg); + for(int col=0; col<3; col++) { + float x=out[col], y=out[col+3]; + out[col] = c*x - s*y; + out[col+3] = s*x + c*y; + } + + // Invert: + Math::trans33(out, out); +} + +void Glue::orient2euler(float* o, float* roll, float* pitch, float* hdg) +{ + // The airplane's "pointing" direction in NED coordinates is vx, + // and it's y (left-right) axis is vy. + float vx[3], vy[3]; + vx[0]=o[0], vx[1]=o[1], vx[2]=o[2]; + vy[0]=o[3], vy[1]=o[4], vy[2]=o[5]; + + // The heading is simply the rotation of the projection onto the + // XY plane + *hdg = Math::atan2(vx[1], vx[0]); + + // The pitch is the angle between the XY plane and vx, remember + // that rotations toward positive Z are _negative_ + float projmag = Math::sqrt(vx[0]*vx[0]+vx[1]*vx[1]); + *pitch = -Math::atan2(vx[2], projmag); + + // Roll is a bit harder. Construct an "unrolled" orientation, + // where the X axis is the same as the "rolled" one, and the Y + // axis is parallel to the XY plane. These two can give you an + // "unrolled" Z axis as their cross product. Now, take the "vy" + // axis, which points out the left wing. The projections of this + // along the "unrolled" YZ plane will give you the roll angle via + // atan(). + float* ux = vx; + float uy[3], uz[3]; + + uz[0] = 0; uz[1] = 0; uz[2] = 1; + Math::cross3(uz, ux, uy); + Math::unit3(uy, uy); + Math::cross3(ux, uy, uz); + + float py = -Math::dot3(vy, uy); + float pz = -Math::dot3(vy, uz); + *roll = Math::atan2(pz, py); +} + +void Glue::geodUp(double* pos, float* out) +{ + double lat, lon, alt; + xyz2geod(pos, &lat, &lon, &alt); + + float slat = Math::sin(lat); + float clat = Math::cos(lat); + float slon = Math::sin(lon); + float clon = Math::cos(lon); + out[0] = clon * clat; + out[1] = slon * clat; + out[2] = slat; +} + +}; // namespace yasim + diff --git a/src/FDM/YASim/Glue.hpp b/src/FDM/YASim/Glue.hpp new file mode 100644 index 000000000..fe6dbf702 --- /dev/null +++ b/src/FDM/YASim/Glue.hpp @@ -0,0 +1,60 @@ +#ifndef _GLUE_HPP +#define _GLUE_HPP + +#include "BodyEnvironment.hpp" + +namespace yasim { + +// The XYZ coordinate system has Z as the earth's axis, the Y axis +// pointing out the equator at zero longitude, and the X axis pointing +// out the middle of the western hemisphere. +class Glue { +public: + static void calcAlphaBeta(State* s, float* alpha, float* beta); + + // Calculates the instantaneous rotation velocities about each + // axis. + static void calcEulerRates(State* s, + float* roll, float* pitch, float* hdg); + + static void xyz2geoc(double* xyz, + double* lat, double* lon, double* alt); + static void geoc2xyz(double lat, double lon, double alt, + double* out); + static void xyz2geod(double* xyz, + double* lat, double* lon, double* alt); + static void geod2xyz(double lat, double lon, double alt, + double* out); + + static double geod2geocLat(double lat); + static double geoc2geodLat(double lat); + + // Returns a global to "local" (north, east, down) matrix. Note + // that the latitude passed in is geoDETic. + static void xyz2nedMat(double lat, double lon, float* out); + + // Conversion between a euler triplet and a matrix that transforms + // "local" (north/east/down) coordinates to the aircraft frame. + static void euler2orient(float roll, float pitch, float hdg, + float* out); + static void orient2euler(float* o, + float* roll, float* pitch, float* hdg); + + // Returns a geodetic (i.e. gravitational, "level", etc...) "up" + // vector for the specified xyz position. + static void geodUp(double* pos, float* out); + +private: + + // WGS84 numbers + static const double EQURAD = 6378137; // equatorial radius + static const double STRETCH = 1.003352810665; // equ./polar radius + + // Derived from the above + static const double SQUASH = 0.99665839311; // 1/STRETCH + static const double POLRAD = 6356823.77346; // EQURAD*SQUASH + static const double iPOLRAD = 1.57311266701e-07; // 1/POLRAD +}; + +}; // namespace yasim +#endif // _GLUE_HPP diff --git a/src/FDM/YASim/Integrator.cpp b/src/FDM/YASim/Integrator.cpp new file mode 100644 index 000000000..872f902c4 --- /dev/null +++ b/src/FDM/YASim/Integrator.cpp @@ -0,0 +1,311 @@ +#include "Math.hpp" +#include "Integrator.hpp" +namespace yasim { + +void Integrator::setBody(RigidBody* body) +{ + _body = body; +} + +void Integrator::setEnvironment(BodyEnvironment* env) +{ + _env = env; +} + +void Integrator::setInterval(float dt) +{ + _dt = dt; +} + +float Integrator::getInterval() +{ + return _dt; +} + +void Integrator::setState(State* s) +{ + _s = *s; +} + +State* Integrator::getState() +{ + return &_s; +} + +// Transforms a "local" vector to a "global" vector (not coordinate!) +// using the specified orientation. +void Integrator::l2gVector(float* orient, float* v, float* out) +{ + Math::tmul33(_s.orient, v, out); +} + +// Updates a position vector for a body c.g. motion with velocity v +// over time dt, from orientation o0 to o1. Because the position +// references the local coordinate origin, but the velocity is that of +// the c.g., this gets a bit complicated. +void Integrator::extrapolatePosition(double* pos, float* v, float dt, + float* o1, float* o2) +{ + // Remember that it's the c.g. that's moving, so account for + // changes in orientation. The motion of the coordinate + // frame will be l2gOLD(cg) + deltaCG - l2gNEW(cg) + float cg[3], tmp[3]; + + _body->getCG(cg); + l2gVector(o1, cg, cg); // cg = l2gOLD(cg) ("cg0") + Math::set3(v, tmp); // tmp = vel + Math::mul3(dt, tmp, tmp); // = vel*dt ("deltaCG") + Math::add3(cg, tmp, tmp); // = cg0 + deltaCG + _body->getCG(cg); + l2gVector(o2, cg, cg); // cg = l2gNEW(cg) ("cg1") + Math::sub3(tmp, cg, tmp); // tmp = cg0 + deltaCG - cg1 + + pos[0] += tmp[0]; // p1 = p0 + (cg0+deltaCG-cg1) + pos[1] += tmp[1]; // (positions are doubles, so we + pos[2] += tmp[2]; // can't use Math::add3) +} + +#if 0 +// A straight euler integration, for reference. Don't use. +void Integrator::calcNewInterval() +{ + float tmp[3]; + State s = _s; + + float dt = _dt / 4; + + orthonormalize(_s.orient); + for(int i=0; i<4; i++) { + _body->reset(); + _env->calcForces(&s); + + _body->getAccel(s.acc); + l2gVector(_s.orient, s.acc, s.acc); + + _body->getAngularAccel(s.racc); + l2gVector(_s.orient, s.racc, s.racc); + + float rotmat[9]; + rotMatrix(s.rot, dt, rotmat); + Math::mmul33(_s.orient, rotmat, s.orient); + + extrapolatePosition(s.pos, s.v, dt, _s.orient, s.orient); + + Math::mul3(dt, s.acc, tmp); + Math::add3(tmp, s.v, s.v); + + Math::mul3(dt, s.racc, tmp); + Math::add3(tmp, s.rot, s.rot); + + _s = s; + } + + _env->newState(&_s); +} +#endif + +void Integrator::calcNewInterval() +{ + // In principle, these could be changed for something other than + // a 4th order integration. I doubt if anyone cares. + const static int NITER=4; + static float TIMESTEP[] = { 1.0, 0.5, 0.5, 1.0 }; + static float WEIGHTS[] = { 6.0, 3.0, 3.0, 6.0 }; + + // Scratch pads: + double pos[NITER][3]; float vel[NITER][3]; float acc[NITER][3]; + float ori[NITER][9]; float rot[NITER][3]; float rac[NITER][3]; + + float *currVel = _s.v; + float *currAcc = _s.acc; + float *currRot = _s.rot; + float *currRac = _s.racc; + + // First off, sanify the initial orientation + orthonormalize(_s.orient); + + for(int i=0; ireset(); + + // FIXME. Copying into a state object is clumsy! The + // per-coordinate arrays should be changed into a single array + // of State objects. Ick. + State stmp; + for(int j=0; j<3; j++) { + stmp.pos[j] = pos[i][j]; + stmp.v[j] = vel[i][j]; + stmp.rot[j] = rot[i][j]; + } + for(int j=0; j<9; j++) + stmp.orient[j] = ori[i][j]; + _env->calcForces(&stmp); + + _body->getAccel(acc[i]); + _body->getAngularAccel(rac[i]); + l2gVector(_s.orient, acc[i], acc[i]); + l2gVector(_s.orient, rac[i], rac[i]); + + // + // Save the resulting derivatives for the next iteration + // + currVel = vel[i]; currAcc = acc[i]; + currRot = rot[i]; currRac = rac[i]; + } + + // Average the resulting derivatives together according to their + // weights. Yes, we're "averaging" rotations, which isn't + // stricly correct -- rotations live in a non-cartesian space. + // But the space is "locally" cartesian. + State derivs; + float tot = 0; + for(int i=0; inewState(&_s); +} + +// Generates a matrix that rotates about axis r through an angle equal +// to (|r| * dt). That is, a rotation effected by rotating with rate +// r for dt "seconds" (or whatever time unit is in use). +// Implementation shamelessly cribbed from the OpenGL specification. +// +// NOTE: we're actually returning the _transpose_ of the rotation +// matrix! This is becuase we store orientations as global-to-local +// transformations. Thus, we want to rotate the ROWS of the old +// matrix to get the new one. +void Integrator::rotMatrix(float* r, float dt, float* out) +{ + // Normalize the vector, and extract the angle through which we'll + // rotate. + float mag = Math::mag3(r); + float angle = dt*mag; + + // Tiny rotations result in degenerate (zero-length) rotation + // vectors, so clamp to an identity matrix. 1e-06 radians + // per 1/30th of a second (typical dt unit) corresponds to one + // revolution per 2.4 days, or significantly less than the + // coriolis rotation. And it's still preserves half the floating + // point precision of a radian-per-iteration rotation. + if(angle < 1e-06) { + out[0] = 1; out[1] = 0; out[2] = 0; + out[3] = 0; out[4] = 1; out[5] = 0; + out[6] = 0; out[7] = 0; out[8] = 1; + return; + } + + float runit[3]; + Math::mul3(1/mag, r, runit); + + float s = Math::sin(angle); + float c = Math::cos(angle); + float c1 = 1-c; + float c1rx = c1*runit[0]; + float c1ry = c1*runit[1]; + float xx = c1rx*runit[0]; + float xy = c1rx*runit[1]; + float xz = c1rx*runit[2]; + float yy = c1ry*runit[1]; + float yz = c1ry*runit[2]; + float zz = c1*runit[2]*runit[2]; + float xs = runit[0]*s; + float ys = runit[1]*s; + float zs = runit[2]*s; + + out[0] = xx+c ; out[3] = xy-zs; out[6] = xz+ys; + out[1] = xy+zs; out[4] = yy+c ; out[7] = yz-xs; + out[2] = xz-ys; out[5] = yz+xs; out[8] = zz+c ; +} + +// Does a Gram-Schmidt orthonormalization on the rows of a +// global-to-local orientation matrix. The order of normalization +// here is x, z, y. This is because of the convention of "x" being +// the forward vector and "z" being up in the body frame. These two +// vectors are the most important to keep correct. +void Integrator::orthonormalize(float* m) +{ + // The 1st, 2nd and 3rd rows of the matrix store the global frame + // equivalents of the x, y, and z unit vectors in the local frame. + float *x = m, *y = m+3, *z = m+6; + + Math::unit3(x, x); // x = x/|x| + + float v[3]; + Math::mul3(Math::dot3(x, z), z, v); // v = z(x dot z) + Math::sub3(z, v, z); // z = z - z*(x dot z) + Math::unit3(z, z); // z = z/|z| + + Math::cross3(z, x, y); // y = z cross x +} + +}; // namespace yasim diff --git a/src/FDM/YASim/Integrator.hpp b/src/FDM/YASim/Integrator.hpp new file mode 100644 index 000000000..323177ced --- /dev/null +++ b/src/FDM/YASim/Integrator.hpp @@ -0,0 +1,58 @@ +#ifndef _INTEGRATOR_HPP +#define _INTEGRATOR_HPP + +#include "BodyEnvironment.hpp" +#include "RigidBody.hpp" + +namespace yasim { + +// +// These objects are responsible for extracting force data from a +// BodyEnvironment object, using a RigidBody object to calculate +// accelerations, and then tying that all together into a new +// "solution" of position/orientation/etc... for the body. The method +// used is a fourth-order Runge-Kutta integration. +// +class Integrator +{ +public: + // Sets the RigidBody that will be integrated. + void setBody(RigidBody* body); + + // Sets the BodyEnvironment object used to calculate the second + // derivatives. + void setEnvironment(BodyEnvironment* env); + + // Sets the time interval between steps. Units can be anything, + // but they must match the other values (if you specify speed in + // m/s, then angular acceleration had better be in 1/s^2 and the + // time interval should be in seconds, etc...) + void setInterval(float dt); + float getInterval(); + + // The current state, i.e. initial conditions for the next + // integration iteration. Note that the acceleration parameters + // in the State object are ignored. + State* getState(); + void setState(State* s); + + // Do a 4th order Runge-Kutta integration over one time interval. + // This is the top level of the simulation. + void calcNewInterval(); + +private: + void orthonormalize(float* m); + void rotMatrix(float* r, float dt, float* out); + void l2gVector(float* orient, float* v, float* out); + void extrapolatePosition(double* pos, float* v, float dt, + float* o1, float* o2); + + BodyEnvironment* _env; + RigidBody* _body; + float _dt; + + State _s; +}; + +}; // namespace yasim +#endif // _INTEGRATOR_HPP diff --git a/src/FDM/YASim/Jet.cpp b/src/FDM/YASim/Jet.cpp new file mode 100644 index 000000000..b4169b69b --- /dev/null +++ b/src/FDM/YASim/Jet.cpp @@ -0,0 +1,59 @@ +#include "Atmosphere.hpp" +#include "Math.hpp" +#include "Jet.hpp" +namespace yasim { + +Jet::Jet() +{ + _rho0 = Atmosphere::getStdDensity(0); + _thrust = 0; + _reheat = 0; +} + +Thruster* Jet::clone() +{ + Jet* j = new Jet(); + j->_thrust = _thrust; + j->_rho0 = _rho0; + return j; +} + +void Jet::setDryThrust(float thrust) +{ + _thrust = thrust; +} + +void Jet::setReheat(float reheat) +{ + _reheat = reheat; +} + +void Jet::getThrust(float* out) +{ + float t = _thrust * _throttle * (_rho / _rho0); + Math::mul3(t, _dir, out); +} + +void Jet::getTorque(float* out) +{ + out[0] = out[1] = out[2] = 0; + return; +} + +void Jet::getGyro(float* out) +{ + out[0] = out[1] = out[2] = 0; + return; +} + +float Jet::getFuelFlow() +{ + return 0; +} + +void Jet::integrate(float dt) +{ + return; +} + +}; // namespace yasim diff --git a/src/FDM/YASim/Jet.hpp b/src/FDM/YASim/Jet.hpp new file mode 100644 index 000000000..c51e30276 --- /dev/null +++ b/src/FDM/YASim/Jet.hpp @@ -0,0 +1,32 @@ +#ifndef _JET_HPP +#define _JET_HPP + +#include "Thruster.hpp" + +namespace yasim { + +// Incredibly dumb placeholder for a Jet implementation. But it does +// what's important, which is provide thrust. +class Jet : public Thruster { +public: + Jet(); + + virtual Thruster* clone(); + + void setDryThrust(float thrust); + void setReheat(float reheat); + + virtual void getThrust(float* out); + virtual void getTorque(float* out); + virtual void getGyro(float* out); + virtual float getFuelFlow(); + virtual void integrate(float dt); + +private: + float _thrust; + float _rho0; + float _reheat; +}; + +}; // namespace yasim +#endif // _JET_HPP diff --git a/src/FDM/YASim/Makefile.am b/src/FDM/YASim/Makefile.am new file mode 100644 index 000000000..c4f7fdcd5 --- /dev/null +++ b/src/FDM/YASim/Makefile.am @@ -0,0 +1,9 @@ +noinst_LIBRARIES = libYASim.a + +libYASim_a_SOURCES = YASim.cxx Airplane.cpp Atmosphere.cpp ControlMap.cpp \ + FGFDM.cpp Gear.cpp Glue.cpp Integrator.cpp Jet.cpp \ + Math.cpp Model.cpp PistonEngine.cpp Propeller.cpp \ + PropEngine.cpp RigidBody.cpp Surface.cpp \ + Thruster.cpp Wing.cpp + +INCLUDES += -I$(top_srcdir) -I$(top_srcdir)/src diff --git a/src/FDM/YASim/Math.cpp b/src/FDM/YASim/Math.cpp new file mode 100644 index 000000000..0af57a03f --- /dev/null +++ b/src/FDM/YASim/Math.cpp @@ -0,0 +1,237 @@ +#include + +#include "Math.hpp" +namespace yasim { + +float Math::abs(float f) +{ + return ::fabs(f); +} + +float Math::sqrt(float f) +{ + return ::sqrt(f); +} + +float Math::pow(float base, float exp) +{ + return ::pow(base, exp); +} + +float Math::ceil(float f) +{ + return ::ceil(f); +} + +float Math::cos(float f) +{ + return ::cos(f); +} + +float Math::sin(float f) +{ + return ::sin(f); +} + +float Math::tan(float f) +{ + return ::tan(f); +} + +float Math::atan2(float y, float x) +{ + return ::atan2(y, x); +} + +double Math::abs(double f) +{ + return ::fabs(f); +} + +double Math::sqrt(double f) +{ + return ::sqrt(f); +} + +double Math::pow(double base, double exp) +{ + return ::pow(base, exp); +} + +double Math::ceil(double f) +{ + return ::ceil(f); +} + +double Math::cos(double f) +{ + return ::cos(f); +} + +double Math::sin(double f) +{ + return ::sin(f); +} + +double Math::tan(double f) +{ + return ::tan(f); +} + +double Math::atan2(double y, double x) +{ + return ::atan2(y, x); +} + +void Math::set3(float* v, float* out) +{ + out[0] = v[0]; + out[1] = v[1]; + out[2] = v[2]; +} + +float Math::dot3(float* a, float* b) +{ + return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; +} + +void Math::mul3(float scalar, float* v, float* out) +{ + out[0] = scalar * v[0]; + out[1] = scalar * v[1]; + out[2] = scalar * v[2]; +} + +void Math::add3(float* a, float* b, float* out) +{ + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + out[2] = a[2] + b[2]; +} + +void Math::sub3(float* a, float* b, float* out) +{ + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + out[2] = a[2] - b[2]; +} + +float Math::mag3(float* v) +{ + return sqrt(dot3(v, v)); +} + + +void Math::unit3(float* v, float* out) +{ + float imag = 1/mag3(v); + mul3(imag, v, out); +} + +void Math::cross3(float* a, float* b, float* out) +{ + float ax=a[0], ay=a[1], az=a[2]; + float bx=b[0], by=b[1], bz=b[2]; + out[0] = ay*bz - by*az; + out[1] = az*bx - bz*ax; + out[2] = ax*by - bx*ay; +} + +void Math::mmul33(float* a, float* b, float* out) +{ + float tmp[9]; + tmp[0] = a[0]*b[0] + a[1]*b[3] + a[2]*b[6]; + tmp[3] = a[3]*b[0] + a[4]*b[3] + a[5]*b[6]; + tmp[6] = a[6]*b[0] + a[7]*b[3] + a[8]*b[6]; + + tmp[1] = a[0]*b[1] + a[1]*b[4] + a[2]*b[7]; + tmp[4] = a[3]*b[1] + a[4]*b[4] + a[5]*b[7]; + tmp[7] = a[6]*b[1] + a[7]*b[4] + a[8]*b[7]; + + tmp[2] = a[0]*b[2] + a[1]*b[5] + a[2]*b[8]; + tmp[5] = a[3]*b[2] + a[4]*b[5] + a[5]*b[8]; + tmp[8] = a[6]*b[2] + a[7]*b[5] + a[8]*b[8]; + + for(int i=0; i<9; i++) + out[i] = tmp[i]; +} + +void Math::vmul33(float* m, float* v, float* out) +{ + float x = v[0], y = v[1], z = v[2]; + out[0] = x*m[0] + y*m[1] + z*m[2]; + out[1] = x*m[3] + y*m[4] + z*m[5]; + out[2] = x*m[6] + y*m[7] + z*m[8]; +} + +void Math::tmul33(float* m, float* v, float* out) +{ + float x = v[0], y = v[1], z = v[2]; + out[0] = x*m[0] + y*m[3] + z*m[6]; + out[1] = x*m[1] + y*m[4] + z*m[7]; + out[2] = x*m[2] + y*m[5] + z*m[8]; +} + +void Math::invert33(float* m, float* out) +{ + // Compute the inverse as the adjoint matrix times 1/(det M). + // A, B ... I are the cofactors of a b c + // d e f + // g h i + float a=m[0], b=m[1], c=m[2]; + float d=m[3], e=m[4], f=m[5]; + float g=m[6], h=m[7], i=m[8]; + + float A = (e*i - h*f); + float B = -(d*i - g*f); + float C = (d*h - g*e); + float D = -(b*i - h*c); + float E = (a*i - g*c); + float F = -(a*h - g*b); + float G = (b*f - e*c); + float H = -(a*f - d*c); + float I = (a*e - d*b); + + float id = 1/(a*A + b*B + c*C); + + out[0] = id*A; out[1] = id*D; out[2] = id*G; + out[3] = id*B; out[4] = id*E; out[5] = id*H; + out[6] = id*C; out[7] = id*F; out[8] = id*I; +} + +void Math::trans33(float* m, float* out) +{ + // 0 1 2 Elements 0, 4, and 8 are the same + // 3 4 5 Swap elements 1/3, 2/6, and 5/7 + // 6 7 8 + out[0] = m[0]; + out[4] = m[4]; + out[8] = m[8]; + + float tmp = m[1]; + out[1] = m[3]; + out[3] = tmp; + + tmp = m[2]; + out[2] = m[6]; + out[6] = tmp; + + tmp = m[5]; + out[5] = m[7]; + out[7] = tmp; +} + +void Math::ortho33(float* x, float* y, + float* xOut, float* yOut, float* zOut) +{ + float x0[3], y0[3]; + set3(x, x0); + set3(y, y0); + + unit3(x0, xOut); + cross3(xOut, y0, zOut); + unit3(zOut, zOut); + cross3(zOut, xOut, yOut); +} + +}; // namespace yasim diff --git a/src/FDM/YASim/Math.hpp b/src/FDM/YASim/Math.hpp new file mode 100644 index 000000000..9407c2a9a --- /dev/null +++ b/src/FDM/YASim/Math.hpp @@ -0,0 +1,70 @@ +#ifndef _MATH_HPP +#define _MATH_HPP + +namespace yasim { + +class Math +{ +public: + // Simple wrappers around library routines + static float abs(float f); + static float sqrt(float f); + static float pow(float base, float exp); + static float ceil(float f); + static float sin(float f); + static float cos(float f); + static float tan(float f); + static float atan2(float y, float x); + + // double variants of the above + static double abs(double f); + static double sqrt(double f); + static double pow(double base, double exp); + static double ceil(double f); + static double sin(double f); + static double cos(double f); + static double tan(double f); + static double atan2(double y, double x); + + // Some 3D vector stuff. In all cases, it is permissible for the + // "out" vector to be the same as one of the inputs. + static void set3(float* v, float* out); + static float dot3(float* a, float* b); + static void cross3(float* a, float* b, float* out); + static void mul3(float scalar, float* v, float* out); + static void add3(float* a, float* b, float* out); + static void sub3(float* a, float* b, float* out); + static float mag3(float* v); + static void unit3(float* v, float* out); + + // Matrix array convention: 0 1 2 + // 3 4 5 + // 6 7 8 + + // Multiply two matrices + static void mmul33(float* a, float* b, float* out); + + // Multiply by vector + static void vmul33(float* m, float* v, float* out); + + // Multiply the vector by the matrix transpose. Or pre-multiply the + // matrix by v as a row vector. Same thing. + static void tmul33(float* m, float* v, float* out); + + // Invert matrix + static void invert33(float* m, float* out); + + // Transpose matrix (for an orthonormal orientation matrix, this + // is the same as the inverse). + static void trans33(float* m, float* out); + + // Generates an orthonormal basis: + // xOut becomes the unit vector in the direction of x + // yOut is perpendicular to xOut in the x/y plane + // zOut becomes the unit vector: (xOut cross yOut) + static void ortho33(float* x, float* y, + float* xOut, float* yOut, float* zOut); +}; + +}; // namespace yasim +#endif // _MATH_HPP diff --git a/src/FDM/YASim/Model.cpp b/src/FDM/YASim/Model.cpp new file mode 100644 index 000000000..5cd77e9cb --- /dev/null +++ b/src/FDM/YASim/Model.cpp @@ -0,0 +1,296 @@ +#include + +#include "Atmosphere.hpp" +#include "Thruster.hpp" +#include "Math.hpp" +#include "RigidBody.hpp" +#include "Integrator.hpp" +#include "Propeller.hpp" +#include "PistonEngine.hpp" +#include "Gear.hpp" +#include "Surface.hpp" +#include "Glue.hpp" + +#include "Model.hpp" +namespace yasim { + +void printState(State* s) +{ + State tmp = *s; + Math::vmul33(tmp.orient, tmp.v, tmp.v); + Math::vmul33(tmp.orient, tmp.acc, tmp.acc); + Math::vmul33(tmp.orient, tmp.rot, tmp.rot); + Math::vmul33(tmp.orient, tmp.racc, tmp.racc); + + printf("\nNEW STATE (LOCAL COORDS)\n"); + printf("pos: %10.2f %10.2f %10.2f\n", tmp.pos[0], tmp.pos[1], tmp.pos[2]); + printf("o: "); + for(int i=0; i<3; i++) { + if(i != 0) printf(" "); + printf("%6.2f %6.2f %6.2f\n", + tmp.orient[3*i+0], tmp.orient[3*i+1], tmp.orient[3*i+2]); + } + printf("v: %6.2f %6.2f %6.2f\n", tmp.v[0], tmp.v[1], tmp.v[2]); + printf("acc: %6.2f %6.2f %6.2f\n", tmp.acc[0], tmp.acc[1], tmp.acc[2]); + printf("rot: %6.2f %6.2f %6.2f\n", tmp.rot[0], tmp.rot[1], tmp.rot[2]); + printf("rac: %6.2f %6.2f %6.2f\n", tmp.racc[0], tmp.racc[1], tmp.racc[2]); +} + +Model::Model() +{ + for(int i=0; i<3; i++) _wind[i] = 0; + + _integrator.setBody(&_body); + _integrator.setEnvironment(this); + + // Default value of 30 Hz + _integrator.setInterval(1.0/30.0); +} + +Model::~Model() +{ + // FIXME: who owns these things? Need a policy +} + +void Model::getThrust(float* out) +{ + float tmp[3]; + out[0] = out[1] = out[2] = 0; + for(int i=0; i<_thrusters.size(); i++) { + Thruster* t = (Thruster*)_thrusters.get(i); + t->getThrust(tmp); + Math::add3(tmp, out, out); + } +} + +void Model::initIteration() +{ + // Precompute torque and angular momentum for the thrusters + for(int i=0; i<3; i++) + _gyro[i] = _torque[i] = 0; + for(int i=0; i<_thrusters.size(); i++) { + Thruster* t = (Thruster*)_thrusters.get(i); + + // Get the wind velocity at the thruster location + float pos[3], v[3]; + t->getPosition(pos); + localWind(pos, _s, v); + + t->setWind(v); + t->setDensity(_rho); + t->integrate(_integrator.getInterval()); + + t->getTorque(v); + Math::add3(v, _torque, _torque); + + t->getGyro(v); + Math::add3(v, _gyro, _gyro); + } +} + +void Model::iterate() +{ + initIteration(); + _body.recalc(); // FIXME: amortize this, somehow + _integrator.calcNewInterval(); +} + +State* Model::getState() +{ + return _s; +} + +void Model::setState(State* s) +{ + _integrator.setState(s); + _s = _integrator.getState(); +} + +RigidBody* Model::getBody() +{ + return &_body; +} + +Integrator* Model::getIntegrator() +{ + return &_integrator; +} + +Surface* Model::getSurface(int handle) +{ + return (Surface*)_surfaces.get(handle); +} + +int Model::addThruster(Thruster* t) +{ + return _thrusters.add(t); +} + +Thruster* Model::getThruster(int handle) +{ + return (Thruster*)_thrusters.get(handle); +} + +void Model::setThruster(int handle, Thruster* t) +{ + _thrusters.set(handle, t); +} + +int Model::addSurface(Surface* surf) +{ + return _surfaces.add(surf); +} + +int Model::addGear(Gear* gear) +{ + return _gears.add(gear); +} + +// The first three elements are a unit vector pointing from the global +// origin to the plane, the final element is the distance from the +// origin (the radius of the earth, in most implementations). So +// (v dot _ground)-_ground[3] gives the distance AGL. +void Model::setGroundPlane(double* planeNormal, double fromOrigin) +{ + for(int i=0; i<3; i++) _ground[i] = planeNormal[i]; + _ground[3] = fromOrigin; +} + +void Model::setAirDensity(float rho) +{ + _rho = rho; +} + +void Model::setAir(float pressure, float temp) +{ + _rho = Atmosphere::calcDensity(pressure, temp); +} + +void Model::setWind(float* wind) +{ + Math::set3(wind, _wind); +} + +void Model::calcForces(State* s) +{ + // Add in the pre-computed stuff. These values aren't part of the + // Runge-Kutta integration (they don't depend on position or + // velocity), and are therefore constant across the four calls to + // calcForces. They get computed before we begin the integration + // step. + _body.setGyro(_gyro); + _body.addTorque(_torque); + for(int i=0; i<_thrusters.size(); i++) { + Thruster* t = (Thruster*)_thrusters.get(i); + float thrust[3], pos[3]; + t->getThrust(thrust); + t->getPosition(pos); + _body.addForce(pos, thrust); + } + + // Gravity, convert to a force, then to local coordinates + float grav[3]; + Glue::geodUp(s->pos, grav); + Math::mul3(-9.8 * _body.getTotalMass(), grav, grav); + Math::vmul33(s->orient, grav, grav); + _body.addForce(grav); + + // Do each surface, remembering that the local velocity at each + // point is different due to rotation. + for(int i=0; i<_surfaces.size(); i++) { + Surface* sf = (Surface*)_surfaces.get(i); + + // Vsurf = wind - velocity + (rot cross (cg - pos)) + float vs[3], pos[3]; + sf->getPosition(pos); + localWind(pos, s, vs); + + float force[3], torque[3]; + sf->calcForce(vs, _rho, force, torque); + _body.addForce(pos, force); + _body.addTorque(torque); + } + + // Get a ground plane in local coordinates. The first three + // elements are the normal vector, the final one is the distance + // from the local origin along that vector to the ground plane + // (negative for objects "above" the ground) + float ground[4]; + ground[3] = localGround(s, ground); + + // Convert the velocity and rotation vectors to local coordinates + float lrot[3], lv[3]; + Math::vmul33(s->orient, s->rot, lrot); + Math::vmul33(s->orient, s->v, lv); + + // The landing gear + for(int i=0; i<_gears.size(); i++) { + float force[3], contact[3]; + Gear* g = (Gear*)_gears.get(i); + g->calcForce(&_body, lv, lrot, ground); + g->getForce(force, contact); + _body.addForce(contact, force); + } +} + +void Model::newState(State* s) +{ + _s = s; + + //printState(s); + + // Some simple collision detection + float ground[4], pos[3], cmpr[3]; + ground[3] = localGround(s, ground); + for(int i=0; i<_gears.size(); i++) { + Gear* g = (Gear*)_gears.get(i); + g->getPosition(pos); + g->getCompression(cmpr); + Math::add3(cmpr, pos, pos); + float dist = ground[3] - Math::dot3(pos, ground); + if(dist < 0) { + printf("CRASH: gear %d\n", i); + *(int*)0=0; + } + } +} + +// Returns a unit "down" vector for the ground in out, and the +// distance from the local origin to the ground as the return value. +// So for a given position V, "dist - (V dot out)" will be the height +// AGL. +float Model::localGround(State* s, float* out) +{ + // Get the ground's "down" vector, this can be in floats, because + // we don't need positioning accuracy. The direction has plenty + // of accuracy after truncation. + out[0] = -(float)_ground[0]; + out[1] = -(float)_ground[1]; + out[2] = -(float)_ground[2]; + Math::vmul33(s->orient, out, out); + + // The distance from the ground to the Aircraft's origin: + double dist = (s->pos[0]*_ground[0] + + s->pos[1]*_ground[1] + + s->pos[2]*_ground[2] - _ground[3]); + + return (float)dist; +} + +// Calculates the airflow direction at the given point and for the +// specified aircraft velocity. +void Model::localWind(float* pos, State* s, float* out) +{ + // Most of the input is in global coordinates. Fix that. + float lwind[3], lrot[3], lv[3]; + Math::vmul33(s->orient, _wind, lwind); + Math::vmul33(s->orient, s->rot, lrot); + Math::vmul33(s->orient, s->v, lv); + + _body.pointVelocity(pos, lrot, out); // rotational velocity + Math::mul3(-1, out, out); // (negated) + Math::add3(lwind, out, out); // + wind + Math::sub3(out, lv, out); // - velocity +} + +}; // namespace yasim diff --git a/src/FDM/YASim/Model.hpp b/src/FDM/YASim/Model.hpp new file mode 100644 index 000000000..a54799d01 --- /dev/null +++ b/src/FDM/YASim/Model.hpp @@ -0,0 +1,80 @@ +#ifndef _MODEL_HPP +#define _MODEL_HPP + +#include "Integrator.hpp" +#include "RigidBody.hpp" +#include "BodyEnvironment.hpp" +#include "util/Vector.hpp" + +namespace yasim { + +// Declare the types whose pointers get passed around here +class Integrator; +class Thruster; +class Surface; +class Gear; + +class Model : public BodyEnvironment { +public: + Model(); + virtual ~Model(); + + RigidBody* getBody(); + Integrator* getIntegrator(); + + State* getState(); + void setState(State* s); + + void iterate(); + + // Externally-managed subcomponents + int addThruster(Thruster* t); + int addSurface(Surface* surf); + int addGear(Gear* gear); + Surface* getSurface(int handle); + Gear* getGear(int handle); + + // Semi-private methods for use by the Airplane solver. + Thruster* getThruster(int handle); + void setThruster(int handle, Thruster* t); + void initIteration(); + void getThrust(float* out); + + // + // Per-iteration settables + // + void setGroundPlane(double* planeNormal, double fromOrigin); + void setWind(float* wind); + void setAirDensity(float rho); + void setAir(float pressure, float temp); + + // BodyEnvironment callbacks + virtual void calcForces(State* s); + virtual void newState(State* s); + +private: + void calcGearForce(Gear* g, float* v, float* rot, float* ground); + float gearFriction(float wgt, float v, Gear* g); + float localGround(State* s, float* out); + void localWind(float* pos, State* s, float* out); + + Integrator _integrator; + RigidBody _body; + + Vector _thrusters; + Vector _surfaces; + Vector _gears; + + double _ground[4]; + float _rho; + float _wind[3]; + + // Accumulators for the total internal gyro and engine torque + float _gyro[3]; + float _torque[3]; + + State* _s; +}; + +}; // namespace yasim +#endif // _MODEL_HPP diff --git a/src/FDM/YASim/PistonEngine.cpp b/src/FDM/YASim/PistonEngine.cpp new file mode 100644 index 000000000..bea871f0b --- /dev/null +++ b/src/FDM/YASim/PistonEngine.cpp @@ -0,0 +1,67 @@ +#include "PistonEngine.hpp" +namespace yasim { + +PistonEngine::PistonEngine(float power, float speed) +{ + // Presume a BSFC (in lb/hour per HP) of 0.45. In SI that becomes + // (2.2 lb/kg, 745.7 W/hp, 3600 sec/hour) 3.69e-07 kg/Ws. + _f0 = power * 3.69e-07; + + _P0 = power; + _omega0 = speed; + + // We must be at sea level under standard conditions + _rho0 = 1.225; // kg/m^3 + + // Further presume that takeoff is (duh) full throttle and + // peak-power, that means that by our efficiency function, we are + // at 11/8 of "ideal" fuel flow. + float realFlow = _f0 * (11.0/8.0); + _mixCoeff = realFlow * 1.1 / _omega0; +} + +void PistonEngine::setThrottle(float t) +{ + _throttle = t; +} + +void PistonEngine::setMixture(float m) +{ + _mixture = m; +} + +void PistonEngine::calc(float density, float speed, + float* torqueOut, float* fuelFlowOut) +{ + // The actual fuel flow + float fuel = _mixture * _mixCoeff * speed; + + // manifold air density + float rho = density * _throttle; + + // How much fuel could be burned with ideal (i.e. uncorrected!) + // combustion. + float burnable = _f0 * (rho/_rho0) * (speed/_omega0); + + // Calculate the fuel that actually burns to produce work. The + // idea is that less than 5/8 of ideal, we get complete + // combustion. We use up all the oxygen at 1 3/8 of ideal (that + // is, you need to waste fuel to use all your O2). In between, + // interpolate. This vaguely matches a curve I copied out of a + // book for a single engine. Shrug. + float burned; + float r = fuel/burnable; + if (burnable == 0) burned = 0; + else if(r < .625) burned = fuel; + else if(r > 1.375) burned = burnable; + else burned = fuel + (burnable-fuel)*(r-.625)*(4.0/3.0); + + // And finally the power is just the reference power scaled by the + // amount of fuel burned. + float power = _P0 * burned/_f0; + + *torqueOut = power/speed; + *fuelFlowOut = fuel; +} + +}; // namespace yasim diff --git a/src/FDM/YASim/PistonEngine.hpp b/src/FDM/YASim/PistonEngine.hpp new file mode 100644 index 000000000..95f43c438 --- /dev/null +++ b/src/FDM/YASim/PistonEngine.hpp @@ -0,0 +1,35 @@ +#ifndef _PISTONENGINE_HPP +#define _PISTONENGINE_HPP + +namespace yasim { + +class PistonEngine { +public: + // Initializes an engine from known "takeoff" parameters. + PistonEngine(float power, float spd); + + void setThrottle(float throttle); + void setMixture(float mixture); + + // Calculates power output and fuel flow, based on a given + // throttle setting (0-1 corresponding to the fraction of + // "available" manifold pressure), mixture (fuel flux per rpm, + // 0-1, where 1 is "max rich", or a little bit more than needed + // for rated power at sea level) + void calc(float density, float speed, + float* powerOut, float* fuelFlowOut); + +private: + float _P0; // reference power setting + float _omega0; // " engine speed + float _rho0; // " manifold air density + float _f0; // "ideal" fuel flow at P0/omega0 + float _mixCoeff; // fuel flow per omega at full mixture + + // Runtime settables: + float _throttle; + float _mixture; +}; + +}; // namespace yasim +#endif // _PISTONENGINE_HPP diff --git a/src/FDM/YASim/PropEngine.cpp b/src/FDM/YASim/PropEngine.cpp new file mode 100644 index 000000000..56ebae51d --- /dev/null +++ b/src/FDM/YASim/PropEngine.cpp @@ -0,0 +1,94 @@ +#include "Math.hpp" +#include "Propeller.hpp" +#include "PistonEngine.hpp" +#include "PropEngine.hpp" +namespace yasim { + +PropEngine::PropEngine(Propeller* prop, PistonEngine* eng, float moment) +{ + // Start off at 500rpm, because the start code doesn't exist yet + _omega = 52.3; + _dir[0] = 1; _dir[1] = 0; _dir[2] = 0; + + _prop = prop; + _eng = eng; + _moment = moment; +} + +PropEngine::~PropEngine() +{ + delete _prop; + delete _eng; +} + +float PropEngine::getOmega() +{ + return _omega; +} + +Thruster* PropEngine::clone() +{ + // FIXME: bloody mess + PropEngine* p = new PropEngine(_prop, _eng, _moment); + cloneInto(p); + p->_prop = new Propeller(*_prop); + p->_eng = new PistonEngine(*_eng); + return p; +} + +void PropEngine::getThrust(float* out) +{ + for(int i=0; i<3; i++) out[i] = _thrust[i]; +} + +void PropEngine::getTorque(float* out) +{ + for(int i=0; i<3; i++) out[i] = _torque[i]; +} + +void PropEngine::getGyro(float* out) +{ + for(int i=0; i<3; i++) out[i] = _gyro[i]; +} + +float PropEngine::getFuelFlow() +{ + return _fuelFlow; +} + +void PropEngine::integrate(float dt) +{ + float speed = -Math::dot3(_wind, _dir); + + float propTorque, engTorque, thrust; + + _eng->setThrottle(_throttle); + _eng->setMixture(_mixture); + + _prop->calc(_rho, speed, _omega, + &thrust, &propTorque); + _eng->calc(_rho, _omega, &engTorque, &_fuelFlow); + + // Turn the thrust into a vector and save it + Math::mul3(thrust, _dir, _thrust); + + // Euler-integrate the RPM. This doesn't need the full-on + // Runge-Kutta stuff. + _omega += dt*(engTorque-propTorque)/Math::abs(_moment); + + // FIXME: Integrate the propeller governor here, when that gets + // implemented... + + // Store the total angular momentum into _gyro + Math::mul3(_omega*_moment, _dir, _gyro); + + // Accumulate the engine torque, it acta on the body as a whole. + // (Note: engine torque, not propeller torque. They can be + // different, but the difference goes to accelerating the + // rotation. It is the engine torque that is felt at the shaft + // and works on the body.) + float tau = _moment < 0 ? engTorque : -engTorque; + Math::mul3(tau, _dir, _torque); +} + +}; // namespace yasim diff --git a/src/FDM/YASim/PropEngine.hpp b/src/FDM/YASim/PropEngine.hpp new file mode 100644 index 000000000..481552a29 --- /dev/null +++ b/src/FDM/YASim/PropEngine.hpp @@ -0,0 +1,42 @@ +#ifndef _PROPENGINE_HPP +#define _PROPENGINE_HPP + +#include "Thruster.hpp" + +namespace yasim { + +class Propeller; +class PistonEngine; + +class PropEngine : public Thruster { +public: + PropEngine(Propeller* prop, PistonEngine* eng, float moment); + virtual ~PropEngine(); + + virtual Thruster* clone(); + + // Dynamic output + virtual void getThrust(float* out); + virtual void getTorque(float* out); + virtual void getGyro(float* out); + virtual float getFuelFlow(); + + // Runtime instructions + virtual void integrate(float dt); + + float getOmega(); + +private: + float _moment; + Propeller* _prop; + PistonEngine* _eng; + + float _omega; // RPM, in radians/sec + float _thrust[3]; + float _torque[3]; + float _gyro[3]; + float _fuelFlow; +}; + +}; // namespace yasim +#endif // _PROPENGINE_HPP diff --git a/src/FDM/YASim/Propeller.cpp b/src/FDM/YASim/Propeller.cpp new file mode 100644 index 000000000..bce03ab8b --- /dev/null +++ b/src/FDM/YASim/Propeller.cpp @@ -0,0 +1,89 @@ +#include "Atmosphere.hpp" +#include "Math.hpp" +#include "Propeller.hpp" +namespace yasim { + +Propeller::Propeller(float radius, float v, float omega, + float rho, float power, float omega0, + float power0) +{ + // Initialize numeric constants: + _lambdaPeak = Math::pow(9.0, -1.0/8.0); + _beta = 1.0/(Math::pow(9.0, -1.0/8.0) - Math::pow(9.0, -9.0/8.0)); + + _r = radius; + _etaC = 0.85; // make this settable? + + _J0 = v/(omega*_lambdaPeak); + + float V2 = v*v + (_r*omega)*(_r*omega); + _F0 = 2*_etaC*power/(rho*v*V2); + + float stallAngle = 0.25; + _lambdaS = _r*(_J0/_r - stallAngle) / _J0; + + // Now compute a correction for zero forward speed to make the + // takeoff performance correct. + float torque0 = power0/omega0; + float thrust, torque; + _takeoffCoef = 1; + calc(Atmosphere::getStdDensity(0), 0, omega0, &thrust, &torque); + _takeoffCoef = torque/torque0; +} + +void Propeller::calc(float density, float v, float omega, + float* thrustOut, float* torqueOut) +{ + float tipspd = _r*omega; + float V2 = v*v + tipspd*tipspd; + + // Clamp v (forward velocity) to zero, now that we've used it to + // calculate V (propeller "speed") + if(v < 0) v = 0; + + float J = v/omega; + float lambda = J/_J0; + + float torque = 0; + if(lambda > 1) { + lambda = 1.0/lambda; + torque = (density*V2*_F0*_J0)/(8*_etaC*_beta*(1-_lambdaPeak)); + } + + // There's an undefined point at 1. Just offset by a tiny bit to + // fix (note: the discontinuity is at EXACTLY one, this is about + // the only time in history you'll see me use == on a floating + // point number!) + if(lambda == 1) lambda = 0.9999; + + // Compute thrust, remembering to clamp lambda to the stall point + float lambda2 = lambda < _lambdaS ? _lambdaS : lambda; + float thrust = (0.5*density*V2*_F0/(1-_lambdaPeak))*(1-lambda2); + + // Calculate lambda^8 + float l8 = lambda*lambda; l8 = l8*l8; l8 = l8*l8; + + // thrust/torque ratio + float gamma = (_etaC*_beta/_J0)*(1-l8); + + // Correct slow speeds to get the takeoff parameters correct + if(lambda < _lambdaPeak) { + // This will interpolate takeoffCoef along a descending from 1 + // at lambda==0 to 0 at the peak, fairing smoothly into the + // flat peak. + float frac = (lambda - _lambdaPeak)/_lambdaPeak; + gamma *= 1 + (_takeoffCoef - 1)*frac*frac; + } + + if(torque > 0) { + torque -= thrust/gamma; + thrust = -thrust; + } else { + torque = thrust/gamma; + } + + *thrustOut = thrust; + *torqueOut = torque; +} + +}; // namespace yasim diff --git a/src/FDM/YASim/Propeller.hpp b/src/FDM/YASim/Propeller.hpp new file mode 100644 index 000000000..eed76450c --- /dev/null +++ b/src/FDM/YASim/Propeller.hpp @@ -0,0 +1,35 @@ +#ifndef _PROPELLER_HPP +#define _PROPELLER_HPP + +namespace yasim { + +// A generic propeller model. See the TeX documentation for +// implementation details, this is too hairy to explain in code +// comments. +class Propeller +{ +public: + // Initializes a propeller with the specified "cruise" numbers + // for airspeed, RPM, power and air density, and two "takeoff" + // numbers for RPM and power (with air speed and density being + // zero and sea level). RPM values are in radians per second, of + // course. + Propeller(float radius, float v, float omega, float rho, float power, + float omega0, float power0); + + void calc(float density, float v, float omega, + float* thrustOut, float* torqueOut); + +private: + float _r; // characteristic radius + float _J0; // zero-thrust advance ratio + float _lambdaS; // "propeller stall" normalized advance ratio + float _F0; // thrust coefficient + float _etaC; // Peak efficiency + float _lambdaPeak; // constant, ~0.759835; + float _beta; // constant, ~1.48058; + float _takeoffCoef; // correction to get the zero-speed torque right +}; + +}; // namespace yasim +#endif // _PROPELLER_HPP diff --git a/src/FDM/YASim/RigidBody.cpp b/src/FDM/YASim/RigidBody.cpp new file mode 100644 index 000000000..5ddded7c2 --- /dev/null +++ b/src/FDM/YASim/RigidBody.cpp @@ -0,0 +1,204 @@ +#include "Math.hpp" +#include "RigidBody.hpp" +namespace yasim { + +RigidBody::RigidBody() +{ + // Allocate space for 16 masses initially. More space will be + // allocated dynamically. + _nMasses = 0; + _massesAlloced = 16; + _masses = new Mass[_massesAlloced]; + _gyro[0] = _gyro[1] = _gyro[2] = 0; + _spin[0] = _spin[1] = _spin[2] = 0; +} + +RigidBody::~RigidBody() +{ + delete[] _masses; +} + +int RigidBody::addMass(float mass, float* pos) +{ + // If out of space, reallocate twice as much + if(_nMasses == _massesAlloced) { + _massesAlloced *= 2; + Mass *m2 = new Mass[_massesAlloced]; + for(int i=0; i<_nMasses; i++) + m2[i] = _masses[i]; + delete[] _masses; + _masses = m2; + } + + _masses[_nMasses].m = mass; + Math::set3(pos, _masses[_nMasses].p); + return _nMasses++; +} + +void RigidBody::setMass(int handle, float mass) +{ + _masses[handle].m = mass; +} + +void RigidBody::setMass(int handle, float mass, float* pos) +{ + _masses[handle].m = mass; + Math::set3(pos, _masses[handle].p); +} + +int RigidBody::numMasses() +{ + return _nMasses; +} + +float RigidBody::getMass(int handle) +{ + return _masses[handle].m; +} + +void RigidBody::getMassPosition(int handle, float* out) +{ + out[0] = _masses[handle].p[0]; + out[1] = _masses[handle].p[1]; + out[2] = _masses[handle].p[2]; +} + +float RigidBody::getTotalMass() +{ + return _totalMass; +} + +// Calcualtes the rotational velocity of a particular point. All +// coordinates are local! +void RigidBody::pointVelocity(float* pos, float* rot, float* out) +{ + Math::sub3(pos, _cg, out); // out = pos-cg + Math::cross3(rot, out, out); // = rot cross (pos-cg) +} + +void RigidBody::setGyro(float* angularMomentum) +{ + Math::set3(angularMomentum, _gyro); +} + +void RigidBody::recalc() +{ + // Calculate the c.g and total mass: + _totalMass = 0; + _cg[0] = _cg[1] = _cg[2] = 0; + for(int i=0; i<_nMasses; i++) { + float m = _masses[i].m; + _totalMass += m; + _cg[0] += m * _masses[i].p[0]; + _cg[1] += m * _masses[i].p[1]; + _cg[2] += m * _masses[i].p[2]; + } + Math::mul3(1/_totalMass, _cg, _cg); + + // Now the inertia tensor: + for(int i=0; i<9; i++) + _I[i] = 0; + + for(int i=0; i<_nMasses; i++) { + float m = _masses[i].m; + + float x = _masses[i].p[0] - _cg[0]; + float y = _masses[i].p[1] - _cg[1]; + float z = _masses[i].p[2] - _cg[2]; + + float xy = m*x*y; float yz = m*y*z; float zx = m*z*x; + float x2 = m*x*x; float y2 = m*y*y; float z2 = m*z*z; + + _I[0] += y2+z2; _I[1] -= xy; _I[2] -= zx; + _I[3] -= xy; _I[4] += x2+z2; _I[5] -= yz; + _I[6] -= zx; _I[7] -= yz; _I[8] += x2+y2; + } + + // And its inverse + Math::invert33(_I, _invI); +} + +void RigidBody::reset() +{ + _torque[0] = _torque[1] = _torque[2] = 0; + _force[0] = _force[1] = _force[2] = 0; +} + +void RigidBody::addForce(float* force) +{ + Math::add3(_force, force, _force); +} + +void RigidBody::addTorque(float* torque) +{ + Math::add3(_torque, torque, _torque); +} + +void RigidBody::addForce(float* pos, float* force) +{ + addForce(force); + + // For a force F at position X, the torque about the c.g C is: + // torque = F cross (C - X) + float v[3], t[3]; + Math::sub3(_cg, pos, v); + Math::cross3(force, v, t); + addTorque(t); +} + +void RigidBody::setBodySpin(float* rotation) +{ + Math::set3(rotation, _spin); +} + +void RigidBody::getCG(float* cgOut) +{ + Math::set3(_cg, cgOut); +} + +void RigidBody::getAccel(float* accelOut) +{ + Math::mul3(1/_totalMass, _force, accelOut); +} + +void RigidBody::getAccel(float* pos, float* accelOut) +{ + getAccel(accelOut); + + // Turn the "spin" vector into a normalized spin axis "a" and a + // radians/sec scalar "rate". + float a[3]; + float rate = Math::mag3(_spin); + Math::set3(_spin, a); + Math::mul3(1/rate, a, a); + + float v[3]; + Math::sub3(_cg, pos, v); // v = cg - pos + Math::mul3(Math::dot3(v, a), a, a); // a = a * (v dot a) + Math::add3(v, a, v); // v = v + a + + // Now v contains the vector from pos to the rotation axis. + // Multiply by the square of the rotation rate to get the linear + // acceleration. + Math::mul3(rate*rate, v, v); + Math::add3(v, accelOut, accelOut); +} + +void RigidBody::getAngularAccel(float* accelOut) +{ + // Compute "tau" as the externally applied torque, plus the + // counter-torque due to the internal gyro. + float tau[3]; // torque + Math::cross3(_gyro, _spin, tau); + Math::add3(_torque, tau, tau); + + // Now work the equation of motion. Use "v" as a notational + // shorthand, as the value isn't an acceleration until the end. + float *v = accelOut; + Math::vmul33(_I, _spin, v); // v = I*omega + Math::cross3(_spin, v, v); // v = omega X I*omega + Math::add3(tau, v, v); // v = tau + (omega X I*omega) + Math::vmul33(_invI, v, v); // v = invI*(tau + (omega X I*omega)) +} + +}; // namespace yasim diff --git a/src/FDM/YASim/RigidBody.hpp b/src/FDM/YASim/RigidBody.hpp new file mode 100644 index 000000000..08e07ef88 --- /dev/null +++ b/src/FDM/YASim/RigidBody.hpp @@ -0,0 +1,123 @@ +#ifndef _RIGIDBODY_HPP +#define _RIGIDBODY_HPP + +namespace yasim { + +// +// A RigidBody object maintains all "internal" state about an object, +// accumulates force and torque information from external sources, and +// calculates the resulting accelerations. +// +// +// Units note: obviously, the choice of mass, time and distance units +// is up to the user. If you provide mass in kilograms, forces in +// newtons, and torques in newton-meters, you'll get your +// accelerations back in m/s^2. The angular units, however, are +// UNIFORMLY radians. Angular velocities are radians per