Modeling Thrust Vectors in Orbiter

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
So I'm having "fun" with those pesky thrust vectors again...

I want to implement accurate two-axis thrust vectoring for a two-engines configuration (like the Centaur). By accurate I mean that the solution should be mathematically exact, and the "normalise" function should not be necessary.

The way I see it a thrust vector is like a conical slice from a sphere which radius is equal to the vector lenght (in Orbiter, 1). The center of the sphere is the thrust vector origin. The angle of the cone is equal to the maximum gimbal range. If we take 10 DEG as an example, then the cone angle will be 10 DEG*2 = 20 DEG.



I'm struggling to model this with the API, because I'm not good at maths and equally not good at coding. It seems rather easy if we take only 1 axis, because then it's a 2D trigonometry problem. If the angle is 10 DEG to the left then we'll have :

X = sin(10 DEG) ~= 0.1736...

and the Z component will be :

Z = cos(10) ~= 0.9848...

So our vector would be :

_V(0.1736, 0, 0.9848)

and of course the more digits we put the more accurate it is. I can "link" that to an input level that ranges from -1 to 1 :

thrust_vector = _V(sin(input_x * 10 DEG), 0, cos(input_x * 10 DEG)

I don't see how to apply this to a 2-axis system. 🤯
 
Last edited:

Zatnikitelman

Addon Developer
Addon Developer
Joined
Jan 13, 2008
Messages
2,311
Reaction score
5
Points
38
Location
Atlanta, GA, USA, North America
It's been a very long time since I did this, I attempted to implement thrust vectoring on both my Solar Service Module and my Prometheus launcher. But I believe, what you would actually need is a rotation matrix. As I recall, you would multiply the rotation matrices for each axis by your thruster's directional vector. Keep in mind the spacecraft's axes you're rotating around. For pitch, you're normally rotating around the X-axis, and for yaw, the Y-axis. So you'd start out with a thruster vector of (0,0,1), then multiply that by the x matrix which takes the form [[1,0,0],[0,cos(d),-sin(d)],[0,sin(d),cos(d)]], then multiply by the y matrix which has the form [[cos(d),0,sin(d)],[0,1,0],[-sin(d),0,cos(d)]] (where d is your angle). The result should be the directional vector of your thruster. So in other words, these would have to be updated each time the angle changes, they aren't static matrices. As I recall, Orbiter's API already has MATRIX3 and VECTOR3 data types and is able to handle simply multiplying them. I think that's accurate, there might be something funky with Orbiter's coordinate system that requires a sign flip somewhere, but otherwise I think that's pretty close.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
@Zatnikitelman : thanks a lot, just to be sure what do you mean by the letter "d" in your formulas, direction ? This is the angle of my rotation, right, so in the above example 10 DEG ?

Also I'm not sure where I should put the input level (value between -1 and 1).

This means a lot for me, many of my projects depend on accurate thrust vectoring and getting this working would be a breakthrough.
 

Face

Addon Developer
Addon Developer
Beta Tester
Joined
Mar 18, 2008
Messages
4,226
Reaction score
177
Points
103
Location
Vienna
Zatnikitelman ninja'ed me to it, but here it goes, anyway:

What you want is rotation in a 3-dimensional system, I think. For this you can either use rotation matrices, euler angles or quaternions.

For rotation matrices, you start with a unity matrix. This would be \( \left( \begin{array}{rrr} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{array}\right) \) . You store this matrix in a variable, let's say \(R\). Then you apply a rotation matrix for a specific axis rotation, let's say the \(Y\) rotation you mentioned in your example. A \(Y\) rotation matrix in Orbiter's left-handed coordinate-system is \( \left( \begin{array}{rrr} cos\varphi & 0 & -sin\varphi \\ 0 & 1 & 0 \\ sin\varphi & 0 & cos\varphi \\ \end{array}\right) \). To calculate the new \(R\), you multiply the old \(R\) with this \(Y\) rotation matrix like so: \(R = R \cdot Y\) . This you do with each consecutive step. Now the clue is that you can do different rotations in sequence, e.g. a rotation around the \(X\) axis, like hinted at in your example. The \(X\) rotation matrix would be \( \left( \begin{array}{rrr} 1 & 0 & 0 \\ 0 & cos\varphi & sin\varphi \\ 0 & -sin\varphi & cos\varphi \\ \end{array}\right) \).

With your current matrix \(R\), you simply multiply it with the original thrust vector \(v_{orig}\) of \(\left( \begin{array}{rrr} 0 \\ 0 \\ 1 \\ \end{array}\right)\), which would be \(v_{curr} = R \cdot v_{orig}\).

For euler angles, you simply define a specific order of rotation, in your case two rotations, first around Y, then around X. With this, you do not have to store the variable \(R\) and manipulate it in each step, because you can determine the resulting matrix for each step by running the 2 multiplications for the two axis. In other words, you do not make small increments like in the rotation matrix case, but "absolute" angles that globally define the thruster gimbal position.
However, the (at first) logical order of rotation around Y, then around X is making it hard for you to enforce the limits of usual gimbal configurations, which is a deviation angle limit, but a 360° "face". For this, an order of rotation around Z first, followed by Y (or X) is better. This would mean that you first rotate your coordinate system around the original vector, which is not limited (except for the obvious 360° overflow to 0°), then do a deviation rotation around any perpedicular axis, which is limited to your deviation limit of e.g. 10°. You can either limit the second axis to only positive values (which would make it necessary for the Z axis to have freedom of 360°), or make it going positive and negative within the limits (which would restrict the Z axis angle to 180°).

Quaternions is similar to rotation matrices, but a bit cryptic to use. In essence it is using an extension of complex numbers to encode the 3D orientation. So instead of a matrix you have a vector of 4 factors.
 

Zatnikitelman

Addon Developer
Addon Developer
Joined
Jan 13, 2008
Messages
2,311
Reaction score
5
Points
38
Location
Atlanta, GA, USA, North America
@Zatnikitelman : thanks a lot, just to be sure what do you mean by the letter "d" in your formulas, direction ? This is the angle of my rotation, right, so in the above example 10 DEG ?

Also I'm not sure where I should put the input level (value between -1 and 1).

This means a lot for me, many of my projects depend on accurate thrust vectoring and getting this working would be a breakthrough.
Yes, d is the angle, keep in mind I believe those functions need the angle in radians.

When you say input level, I assume you mean "if it's full up pitch (1) then rotate the full 10 degrees up, if half down pitch (-0.5) then rotate half down?" If so, then you'd "map" the input level to degrees, 1 = 10 degrees, -1 = -10 degrees. Which gives the equation "Degrees = 10 * InputLevel"

I'm happy to help! I just kind of sort of started poking my head back in here, so in a way thank YOU! Digging back into my memories for how do this was kind of fun!

Also, Face's answer is really good and better grounded in the theory behind it than me, his response might help with some of the "why" behind this.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
It... it worked !! Awesome !

The controls are inverted, but that should be easy to fix.

The force vector in Orbiter is perfectly consistent, it keeps the same lenght during the gimbal process. This is exactly what I wanted to do. Even better, my engines boosters have a 4.5 DEG tilt outwards. I had no idea how to do it. Now I simply have to translate those 4.5 DEG in the first vector. 🥳


Code:
    VECTOR3 YF77_dir = _V(0, 0, 1);

    MATRIX3 matrix_x = {
        1, 0, 0,
        0, cos(GIMBAL_ANGLE*-P),-sin(GIMBAL_ANGLE*-P),
        0, sin(GIMBAL_ANGLE*-P), cos(GIMBAL_ANGLE*-P)
    };
    MATRIX3 matrix_y = {
        cos(GIMBAL_ANGLE*-Y),0,sin(GIMBAL_ANGLE*-Y),
        0,1,0,
        -sin(GIMBAL_ANGLE*-Y),0,cos(GIMBAL_ANGLE*-Y),
    };

    MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

    VECTOR3 output_vec = tmul(matrix_xy, YF77_dir);

    SetThrusterDir(th_YF77[0], output_vec);
    SetThrusterDir(th_YF77[1], output_vec);

Edit : I put a "minus" before my P (pitch) and Y (yaw) inputs, and now the controls are perfect ! Also for optimization's sake I think I could safely move the first direction vector in the header file, as it is static.
 
Last edited:

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
I managed to implement this for each of my 4 radial boosters, which gives very good control on pitch and yaw. Each pair of opposed have a slighltly different matrix where the pitch and yaw inputs are inverted. Many thanks for the input, I think I'm beginning to grasp the matrix concept and how powerful it is. 👍

So we have quite a robust 2-axis and pretty universal TVC model, which is really great. 🥳

Next logical question : how to implement the Roll channel into this (for a 2 engines Centaur-like configuration) ? It means we have to transition from a 2-axis to a 3-axis model, right ? Does it mean we have to go into more complexes quaternion matrixes (I hope not !) ? The problem I see is that the Roll channel will sort of compete with Pitch and Yaw channels for engine control... I guess it can be translated in vectors/matrixes again, but how ?

And "to go further" (as they say in school math books) : is there a way to generalize this to a n-engine configuration ? Assuming that the engines configuration is a regular n-gon with equal angles. Of course something like a Saturn-V would count as a 4-engines configuration, because the 5th engine is central and can't be used for roll control (it stays in two-axis mode, helping to keep trajectory straight if the other engines are "busy" correcting roll).

Then we could wrap this into a "ready-to-use" sample of code and stick it somewhere on the SDK forum, I think it would help many people. That's the kind of maths->physics->engineering->code spirit that defines Orbiter :hailprobe:
 
Last edited:

Zatnikitelman

Addon Developer
Addon Developer
Joined
Jan 13, 2008
Messages
2,311
Reaction score
5
Points
38
Location
Atlanta, GA, USA, North America
I hit the same problem when I was working on the same thing. You're right, mixing the roll with pitch and/or yaw is tricky. On its own, roll is inverting the direction the other engine gimbals on the axis. That part is fairly simple, use two separate rotation matrices and invert the sign of the angle on one. As far as mixing, again, I had to think through and logically map the angles to each other. I'll just use the example I did of just using two engines in the X-axis (i.e. left and right side engines). At full pitch up, they'd be fully deflected up. But at full pitch up and full roll right, the left one would be fully deflected up, while the right one would be at 0 deflection. Then if you have any yaw, that should be able to be handled normally. I think how I handled the roll/pitch was to calculate the pitch angle before doing anything with the matrices. So then in that example, the pitch angles that go into the matrices are 10 for the left, and 0 for the right. Then for your engines rotated 90 degrees, you'd do the same except you'd be mixing roll and yaw.

As far as generalizing it, I'm sure it could be done, roll would be handled by having the engines gimbal tangent to the polygon, but what about pitch and yaw? I've done some quick Googling, and I don't know how many rockets use more than a few engines for control. For example the Saturn 1 and 1b with 8 engines, only gimbaled the outer 4 which unless they're at the 45 degree points around the Z axis, is the same basic pitch/yaw profile that you're working on.
 

Face

Addon Developer
Addon Developer
Beta Tester
Joined
Mar 18, 2008
Messages
4,226
Reaction score
177
Points
103
Location
Vienna
I think the roll problem is not a rotation matrix problem, but a force vector problem. In essence the whole thrust vectoring thing is a force vector problem, it just happens so that pitch and yaw is easily translated to equivalent gimbal deflections on standard thruster layouts. What you want is a rotation of the vessel around a certain axis, and for that you rotate thrust vectors to achieve rotation momentum.
It is not a question of dimension, because you are already in the 3rd dimension with your euler angle solution. AFAICS, you currently use 2 angles as input and get a thruster direction as output, regardless of where these thrusters are positioned. This only works as long as your thrusters are in a standard layout, i.e. "behind" the center of gravity. Imagine a 2-engine layout where one thruster is at the bow (still pointing backwards) and one at the stern. Your solution would not induce a pitch or yaw in this configuration (or at least only a small one), because the lockstepped force vectors would cancel out due to counter-rotations.
So if you want to create a universal solution for the problem, you have to take the thruster positioning itself into account. It would not be as easy as saying "my joystick commands a deflection of 10°, so I do this math of 10° to all thrusters". It would be more of saying "my joystick commands a rotation of magnitude n around axis A, so how can I achieve this with my given array of thruster positions, perhaps in a way that deflects them the least". I think this is not trivial to do for a general setup. I'm not even sure if it always converges to a single solution. My guess is that the general setup is a bit like inverse kinematics for robotics.
 
Last edited:

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
Alright so the idea is to make a "mixing function" between say the pitch channel and the roll channel. From what I understand from Atlantis samples (a 3-engines configuration), Martins did it that way.

The trick is that to achieve a "clean" roll manoeuver your thrusters have to move in exact opposite directions, else you generate a momentum that is unlikely to be "pure pitch". This is not easy.

The only way to avoid unwanted to momentum is to have the engines (2, 4, 7 or n...) deflection vectors end that way, so that you can draw a line from their ends that crosses the x/y origin - the purple straight dotted line below - (for that z doesn't matter, it will work regardless the engines are at the bow or the aft).

If you "raise" an engine only, it will generate X and Y momentum, which is bad.

(hand-made drawing, not perfectly scaled) :


So maybe the idea is to impement some kind of "priority", given that roll manoeuvers are not very frequent during a rocket launch, and usually the "roll program" starts right after liftoff, so that the rocket can pitch down (or up, like the Space Shuttle) to the desired launch azimuth. From there roll control is mostly about minor corrections, but what worries me is that I have to "interrupt" the pitch/yaw program to do those corrections. Maybe I could track the roll angle and angular velocity and decide that beyond a given thresold, a roll correction should be done, then resume the pitch/yaw program.

Now this is not a huge issue for my current Long March 5 rocket, because each radial booster has 2 engines, meaning the 4 outer ones can have a lot of those "interruptions" while the 4 inner ones and the core focus purely on pitch and yaw. After separation the core has to handle roll alone though ; and the second stage is pretty much a chinese Centaur.

Also those large fins with control surfaces help and it shows how much that roll problem was an issue for the chinese rocket engineers, looks like one more safeguard. Now of course fins work only when there is enough dynamic pressure.

Also here's the code I wrote for the boosters modules engines control (we have 4 of them), remember I'm a pure hobbyist, and if you see ways to improve it, please feel free to do so. It is yours as much it is mine. What I can say is that it works, I can pilot the rocket manually and have enough control to start writing an ascent program :

Note : I have to add the time dimension into that, because even if those actuators can be very reactive I want to add some inertia into the system.

C++:
    char *myName = GetName();

    if (strcmp(myName, "CZ-5E-K-3-1-1") == 0)
    {
        if (separation == 1)
        {
            P = 1; // flyaway values
            Y = 0;
            R = 0;
        }

        VECTOR3 YF100_dir = _V(0, -0.0784591, 0.996917);

        // inner engine

        MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE*-P),-sin(GIMBAL_ANGLE*-P),
            0, sin(GIMBAL_ANGLE*-P), cos(GIMBAL_ANGLE*-P)
        };
        MATRIX3 matrix_y = {
            cos(GIMBAL_ANGLE*-Y),0,sin(GIMBAL_ANGLE*-Y),
            0,1,0,
            -sin(GIMBAL_ANGLE*-Y),0,cos(GIMBAL_ANGLE*-Y),
        };

        MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

        vec_K311 = tmul(matrix_xy, YF100_dir);

        SetThrusterDir(th_YF100[1], vec_K311);

        // outer engine "roll mode"

        if (R != 0) // we have a roll input
        {

            MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE*0),-sin(GIMBAL_ANGLE*0),
            0, sin(GIMBAL_ANGLE*0), cos(GIMBAL_ANGLE*0)
            };
            MATRIX3 matrix_y = {
                cos(GIMBAL_ANGLE*R),0,sin(GIMBAL_ANGLE*R),
                0,1,0,
                -sin(GIMBAL_ANGLE*R),0,cos(GIMBAL_ANGLE*R),
            };

            MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

            vec_K311 = tmul(matrix_xy, YF100_dir);

            SetThrusterDir(th_YF100[0], vec_K311);
        }
        else
        {
            SetThrusterDir(th_YF100[0], vec_K311);
        }

    }
    if (strcmp(myName, "CZ-5E-K-3-1-2") == 0)
    {

        if (separation == 1)
        {
            P = 1; // flyaway value
        }

        VECTOR3 YF100_dir = _V(0, -0.0784591, 0.996917);

        MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE*-P),-sin(GIMBAL_ANGLE*-P),
            0, sin(GIMBAL_ANGLE*-P), cos(GIMBAL_ANGLE*-P)
        };
        MATRIX3 matrix_y = {
            cos(GIMBAL_ANGLE*-Y),0,sin(GIMBAL_ANGLE*-Y),
            0,1,0,
            -sin(GIMBAL_ANGLE*-Y),0,cos(GIMBAL_ANGLE*-Y),
        };

        MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

        VECTOR3 output_vec = tmul(matrix_xy, YF100_dir);
        vec_K312 = tmul(matrix_xy, YF100_dir);

        SetThrusterDir(th_YF100[1], vec_K312);

        if (R != 0) // we have a roll input
        {

            MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE * 0),-sin(GIMBAL_ANGLE * 0),
            0, sin(GIMBAL_ANGLE * 0), cos(GIMBAL_ANGLE * 0)
            };
            MATRIX3 matrix_y = {
                cos(GIMBAL_ANGLE * R),0,sin(GIMBAL_ANGLE * R),
                0,1,0,
                -sin(GIMBAL_ANGLE * R),0,cos(GIMBAL_ANGLE * R),
            };

            MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

            vec_K312 = tmul(matrix_xy, YF100_dir);

            SetThrusterDir(th_YF100[0], vec_K312);
        }
        else
        {
            SetThrusterDir(th_YF100[0], vec_K312);
        }
    }
    if (strcmp(myName, "CZ-5E-K-3-1-3") == 0)
    {
        if (separation == 1)
        {
            P = 1; // flyaway value
        }

        VECTOR3 YF100_dir = _V(0, -0.0784591, 0.996917);

        MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE*P),-sin(GIMBAL_ANGLE*P),
            0, sin(GIMBAL_ANGLE*P), cos(GIMBAL_ANGLE*P)
        };
        MATRIX3 matrix_y = {
            cos(GIMBAL_ANGLE*Y),0,sin(GIMBAL_ANGLE*Y),
            0,1,0,
            -sin(GIMBAL_ANGLE*Y),0,cos(GIMBAL_ANGLE*Y),
        };

        MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

        vec_K313 = tmul(matrix_xy, YF100_dir);

        SetThrusterDir(th_YF100[1], vec_K313);

        if (R != 0) // we have a roll input
        {

            MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE * 0),-sin(GIMBAL_ANGLE * 0),
            0, sin(GIMBAL_ANGLE * 0), cos(GIMBAL_ANGLE * 0)
            };
            MATRIX3 matrix_y = {
                cos(GIMBAL_ANGLE*R),0,sin(GIMBAL_ANGLE*R),
                0,1,0,
                -sin(GIMBAL_ANGLE*R),0,cos(GIMBAL_ANGLE*R),
            };

            MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

            vec_K313 = tmul(matrix_xy, YF100_dir);

            SetThrusterDir(th_YF100[0], vec_K313);
        }
        else
        {
            SetThrusterDir(th_YF100[0], vec_K313);
        }
    }
    if (strcmp(myName, "CZ-5E-K-3-1-4") == 0)
    {

        if (separation == 1)
        {
            P = 1; // flyaway value
        }

        VECTOR3 YF100_dir = _V(0, -0.0784591, 0.996917);

        MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE*P),-sin(GIMBAL_ANGLE*P),
            0, sin(GIMBAL_ANGLE*P), cos(GIMBAL_ANGLE*P)
        };
        MATRIX3 matrix_y = {
            cos(GIMBAL_ANGLE*Y),0,sin(GIMBAL_ANGLE*Y),
            0,1,0,
            -sin(GIMBAL_ANGLE*Y),0,cos(GIMBAL_ANGLE*Y),
        };

        MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

        vec_K314 = tmul(matrix_xy, YF100_dir);

        SetThrusterDir(th_YF100[1], vec_K314);
        
        if (R != 0) // we have a roll input
        {

            MATRIX3 matrix_x = {
            1, 0, 0,
            0, cos(GIMBAL_ANGLE * 0),-sin(GIMBAL_ANGLE * 0),
            0, sin(GIMBAL_ANGLE * 0), cos(GIMBAL_ANGLE * 0)
            };
            MATRIX3 matrix_y = {
                cos(GIMBAL_ANGLE*R),0,sin(GIMBAL_ANGLE*R),
                0,1,0,
                -sin(GIMBAL_ANGLE*R),0,cos(GIMBAL_ANGLE*R),
            };

            MATRIX3 matrix_xy = mul(matrix_x, matrix_y);

            vec_K314 = tmul(matrix_xy, YF100_dir);

            SetThrusterDir(th_YF100[0], vec_K314);
        }
        else
        {
            SetThrusterDir(th_YF100[0], vec_K314);
        }
    }
 
Last edited:

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
Also I was wondering : how can I translate the output thrust vector into an animation value (which should between 0 and 1) ?

If I take the x or y components of my output vector, I have of course very small values... How can I scale that back to a 0 to 1 range ?

Again there's a mathematic tool I don't have...
 

asbjos

tuanibrO
Addon Developer
Joined
Jun 22, 2011
Messages
659
Reaction score
151
Points
58
Location
This place called "home".
About rolling at the same time that you're pitching/yawing: this was one of the few things in my Project Mercury addon that were un-physical. I did the same thing as you (I think), by separating into
Code:
if (roll != 0.0)
{
    animate twisted motion
}
else // not rolling, so pitching and/or yawing
{
    animate x-axis for yaw
    animate y-axis for pitch
}

This is not easy, and I guess that is part of the reason that (as you also note) most rocket launches have separated into first a roll program phase, and thereafter a pitch program phase.
In the Mercury-Atlas autopilot, I didn't implement any rolling after the end of the roll program (from T+2 s to T+15 s). Any errors in azimuth are instead corrected by yawing. The resulting error in roll (i.e. not being horizontal level) are negligible (sub 10 degrees), and therefore don't affect the function of pitching and yawing.
Had the roll error been e.g. 90 degrees, any rocket pitching would instead give a change in yaw, but this is then not the case for this low roll error.

For animating (and general control when in manual), I implemented the rocket controls as control surfaces with 0 area (i.e. no aerodynamic effect), using the Orbiter SetControlSurfaceLevel() functions. While in manual mode, the control surfaces are controlled by the users default numpad buttons, just like the DeltaGlider. While in autopilot, you can set the level with the SetControl... function. This has the added benefit that Orbiter natively implements latency in the controls.
So if you pitch up, the control surface level is 1. If you then lift your finger from the pitch up button, Orbiter natively gradually decreases from 1.0 to 0.0.

Using the GetControlSurfaceLevel() function, I then mapped the physical deflection of the 0 area control surface to the engine directions (SetThrusterDir()) and animations (SetAnimation()).

The code is bundled in the addon, or on GitHub: https://github.com/asbjos/ProjectMercuryX/blob/master/MercuryAtlas/MercuryAtlas/MercuryAtlas.cpp
Animation done in clbkPostStep (currently lines 1066 to 1082), engine directioning while manual control in AtlasEngineDir, and autopilot in AtlasAutopilot (you can ignore the euler-stuff, as that is only used when time accelerating, when the physical implementation becomes unstable).

The rocket control was among the first things I did during the project, and therefore it's somewhat chaotic and possibly badly commented. Sorry.

If you ever do launch complexes / launchpads, I would also recommend https://github.com/asbjos/ProjectMercuryX/blob/master/LC14/LC14/LC14.cpp
It does most of the things you can imagine a launch complex doing, including lights, mesh manipulation (oapiEditMeshGroup), animations, camera views, deflecting engine smoke, holding the rocket down, etc.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
For animating (and general control when in manual), I implemented the rocket controls as control surfaces with 0 area (i.e. no aerodynamic effect), using the Orbiter SetControlSurfaceLevel() functions. While in manual mode, the control surfaces are controlled by the users default numpad buttons, just like the DeltaGlider. While in autopilot, you can set the level with the SetControl... function. This has the added benefit that Orbiter natively implements latency in the controls.
So if you pitch up, the control surface level is 1. If you then lift your finger from the pitch up button, Orbiter natively gradually decreases from 1.0 to 0.0.

Ah that's very smart !! :hailprobe: My core stage has no fins so it will work perfectly !

The resulting error in roll (i.e. not being horizontal level) are negligible (sub 10 degrees), and therefore don't affect the function of pitching and yawing.

Now you're saying that, those big fins on the Long March 5 make a lot of sense... As the rocket ends the roll program, it has already built enough speed for the fins to do the stabilizing job. At MaxQ it is extremely stable, and as you say after that there's no reason for the rocket to suddenly roll (unless it loses an engine) (y)

Also by Googleing I found your Mercury Project X already, very nice job you did there ! :cheers:
 

Zatnikitelman

Addon Developer
Addon Developer
Joined
Jan 13, 2008
Messages
2,311
Reaction score
5
Points
38
Location
Atlanta, GA, USA, North America
Alright so the idea is to make a "mixing function" between say the pitch channel and the roll channel. From what I understand from Atlantis samples (a 3-engines configuration), Martins did it that way.
Yes, exactly!
The trick is that to achieve a "clean" roll manoeuver your thrusters have to move in exact opposite directions, else you generate a momentum that is unlikely to be "pure pitch". This is not easy

The only way to avoid unwanted to momentum is to have the engines (2, 4, 7 or n...) deflection vectors end that way, so that you can draw a line from their ends that crosses the x/y origin - the purple straight dotted line below - (for that z doesn't matter, it will work regardless the engines are at the bow or the aft).

If you "raise" an engine only, it will generate X and Y momentum, which is bad.

(hand-made drawing, not perfectly scaled) :
Why not? Thrust commands in Orbiter can be treated as a VECTOR3 of the values for how much thrust each axis should provide. So full pitch up would be (1,0,0), full pitch down would be (-1,0,0), half thrust roll would be (0,0,0.5) etc. If you're mixing pitch and roll or yaw and roll, then you can get both at the same time. If there isn't a pitch component commanded, then a full roll would move the thrusters in opposite directions.
So maybe the idea is to impement some kind of "priority", given that roll manoeuvers are not very frequent during a rocket launch, and usually the "roll program" starts right after liftoff, so that the rocket can pitch down (or up, like the Space Shuttle) to the desired launch azimuth. From there roll control is mostly about minor corrections, but what worries me is that I have to "interrupt" the pitch/yaw program to do those corrections. Maybe I could track the roll angle and angular velocity and decide that beyond a given thresold, a roll correction should be done, then resume the pitch/yaw program.

Now this is not a huge issue for my current Long March 5 rocket, because each radial booster has 2 engines, meaning the 4 outer ones can have a lot of those "interruptions" while the 4 inner ones and the core focus purely on pitch and yaw. After separation the core has to handle roll alone though ; and the second stage is pretty much a chinese Centaur.

Also those large fins with control surfaces help and it shows how much that roll problem was an issue for the chinese rocket engineers, looks like one more safeguard. Now of course fins work only when there is enough dynamic pressure.

Also here's the code I wrote for the boosters modules engines control (we have 4 of them), remember I'm a pure hobbyist, and if you see ways to improve it, please feel free to do so. It is yours as much it is mine. What I can say is that it works, I can pilot the rocket manually and have enough control to start writing an ascent program :

Note : I have to add the time dimension into that, because even if those actuators can be very reactive I want to add some inertia into the system.
Are you still talking purely engine gimbaling or autopilot design? The latter is another beast. But if talking about that, I had to do something like what you're talking about on my Prometheus launcher (the autopilot never made it to production). Basically the launcher would lift to a certain altitude (clear the tower) then roll to the desired heading, then pitch over while maintaining -180 degrees of roll. But as I never got it quite working right, I'm probably not the best reference on autopilot design.
Also I was wondering : how can I translate the output thrust vector into an animation value (which should between 0 and 1) ?

If I take the x or y components of my output vector, I have of course very small values... How can I scale that back to a 0 to 1 range ?

Again there's a mathematic tool I don't have...
Your animation would define the engine animation as being 20 degrees, and starting at 0.5, correct? Then you'd just map the axis command to animation state. So -1 = 0, 0 = 0.5, 1 = 1 or [Animation_State = 0.5*(axis_command) + 0.5].

Now, that gets you partway there, but Orbiter doesn't easily let you animate a single piece of the mesh in multiple axes. You could do some fancy math and rotate the axis of the animation, but I'm not even sure how to calculate that, Face might know.

The other way, and I did this for Prometheus's launchpad, was to hide a small mesh group, and set the engine as a child of that with its own animation. So maybe create a little tetrahedron buried somewhere invisible (or with a transparent material?) and let that be your pitch animation. Then the engine itself is a child animation that responds to yaw. The hidden mesh would have to be at the center of the rotation for both pitch and yaw for that engine, but most engines rotate around a point that's pretty deep "inside" the assembly so hopefully it won't be visible.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
Now, that gets you partway there, but Orbiter doesn't easily let you animate a single piece of the mesh in multiple axes

There I used parent animations. The result might not be ultra-accurate but it works rather well.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
So I'm using asbjos concept with great success, those "Control Surfaces" are indeed more than just that and can be used to simulate quite a lot of mechanical systems that have some kind of inertia. :cool:

We could even simulate different responsivity levels on each pitch, yaw and roll channels.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
So now I have pitch/yaw working the way I want, I'm giving another thought to the 'roll problem'. I think I can work something out, better than the 'priority' system. Not currently at home but I'll give it a try this evening.
 
Last edited:

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
@asbjos : what about this ? From an animation standpoint, it works perfectly (for a 2-engines config where engines are aligned on the pitch axis) :

Code:
    // Engines gimbal animations

        Plvl = GetControlSurfaceLevel(AIRCTRL_ELEVATOR);
        Ylvl = GetControlSurfaceLevel(AIRCTRL_RUDDER);
        Rlvl = GetControlSurfaceLevel(AIRCTRL_AILERON);

        SetAnimation(anim_pitch_eng1, Plvl / 2 + 0.5);
        SetAnimation(anim_yaw_eng1, (Ylvl+Rlvl) / 2 + 0.5);

        SetAnimation(anim_pitch_glow1, Plvl / 2 + 0.5);
        SetAnimation(anim_yaw_glow1, Ylvl / 2 + 0.5);

        SetAnimation(anim_pitch_eng2, Plvl / 2 + 0.5);
        SetAnimation(anim_yaw_eng2, (Ylvl-Rlvl) / 2 + 0.5);

        SetAnimation(anim_pitch_glow2, Plvl / 2 + 0.5);
        SetAnimation(anim_yaw_glow2, Ylvl / 2 + 0.5);

I'm going to try to feed the matrix with something similar, I'll keep you updated.
 

asbjos

tuanibrO
Addon Developer
Joined
Jun 22, 2011
Messages
659
Reaction score
151
Points
58
Location
This place called "home".
GetControlSurfaceLevel returns -1 to 1. SetAnimation accepts 0 to 1.

If you have full yaw and full roll, you get
C++:
SetAnimation(anim_yaw_eng1, (1.0+1.0) / 2 + 0.5);
I.e. SetAnimation(1.5).

But I can look at the problem again the next days. I'm also interested in finding a physical/general solution to this problem.
 

N_Molson

Addon Developer
Addon Developer
Donator
Addon List Curator
Joined
Mar 5, 2010
Messages
7,544
Reaction score
687
Points
188
Location
Toulouse
It works ! We did it !! We have normal vectors and Pitch Yaw & Roll control on a given angle !! 🥳🥳

I had to write a separate matrix for each one of the two engines, because they have to react differently to the Roll input, of course. I added a DebugString for vector size control. As you can see on the pic below, the result is amazing : I can pitch, yaw and roll any combination, the vectors stay normal, and with 6 digits, I think we're good ! I don't even need to use normalise ! 🥳

So here's the code, note you'll have to map Roll on the pitch channel instead of yaw if your engines are aligned the other way :

C++:
VECTOR3 YF77_dir = _V(0, 0, 1); // reference direction of the engines

    // we need separate matrixes to implement the roll channel (mixed with yaw)

    // engine 1 matrixes

    MATRIX3 matrix_x_eng1 = {
        1, 0, 0,  
        0, cos(GIMBAL_ANGLE*-Plvl),-sin(GIMBAL_ANGLE*-Plvl),
        0, sin(GIMBAL_ANGLE*-Plvl), cos(GIMBAL_ANGLE*-Plvl)
    };
    MATRIX3 matrix_y_eng1 = {
        cos(GIMBAL_ANGLE*-(-Ylvl + Rlvl)/2),0,sin(GIMBAL_ANGLE*-(-Ylvl+Rlvl)/2),
        0,1,0,
        -sin(GIMBAL_ANGLE*-(-Ylvl + Rlvl)/2),0,cos(GIMBAL_ANGLE*-(-Ylvl + Rlvl)/2),
    };

    MATRIX3 matrix_xy_eng1 = mul(matrix_x_eng1, matrix_y_eng1);

    VECTOR3 output_vec_eng1 = tmul(matrix_xy_eng1, YF77_dir);

    SetThrusterDir(th_YF77[0], output_vec_eng1);

    // engine 2 matrixes

    MATRIX3 matrix_x_eng2 = {
    1, 0, 0,
    0, cos(GIMBAL_ANGLE*-Plvl),-sin(GIMBAL_ANGLE*-Plvl),
    0, sin(GIMBAL_ANGLE*-Plvl), cos(GIMBAL_ANGLE*-Plvl)
    };
    MATRIX3 matrix_y_eng2 = {
        cos(GIMBAL_ANGLE*-(-Ylvl - Rlvl) / 2),0,sin(GIMBAL_ANGLE*-(-Ylvl - Rlvl) / 2),
        0,1,0,
        -sin(GIMBAL_ANGLE*-(-Ylvl - Rlvl) / 2),0,cos(GIMBAL_ANGLE*-(-Ylvl - Rlvl) / 2),
    };

    MATRIX3 matrix_xy_eng2 = mul(matrix_x_eng2, matrix_y_eng2);

    VECTOR3 output_vec_eng2 = tmul(matrix_xy_eng2, YF77_dir);

    SetThrusterDir(th_YF77[1], output_vec_eng2);

    // checking that we have normal vectors (x²+y²+z² should be equal to 1)

    sprintf(oapiDebugString(), "vec1 size = %.6f || vec2 size = %.6f",
        pow(output_vec_eng1.x, 2) + pow(output_vec_eng1.y, 2) + pow(output_vec_eng1.z, 2),
        pow(output_vec_eng2.x, 2) + pow(output_vec_eng2.y, 2) + pow(output_vec_eng2.z, 2)
    );

 
Last edited:
Top