Michael Isner's 10th Level Magic User Quaternion Spells

(Solving rotation problems in XSI 2.0 or greater)

Contents:
Visualizing Rotations
Comparison of Rotation Representations
Using Quaternions without scripting
Techniques to Visualize Quaternion Operations
Extracting Quaternions from Euler
Reading Quaternions Directly
Adding 2 Quaternions
Blending 2 Quaternions
Reversing Rotations
Subtracting Quaternions
Scaling Rotations
Constraining Rotations to a plane


1) Visualizing rotations

Most peoples first thought of a rotation is either: a general concept of how something is angled, where something is points, how something like a basketball is oriented or even a the (x,y,z) coordinates of a rotation.  None of these are really precise enough to be productive with rotations.  To solve problems with rotations you really need to be clear in your head about what you are dealing with.

So to be specific, lets look in detail at the properties of rotations.  This approach is not the only way you can picture a rotation, but it's the method I've found the best to understand rotations and solve rotation problems.

If you want to track a rotation with your eye, you usually fix on a single point to follow it.  This point on a unit sphere, from here on will be called a "Unit Point".
 


You can picture as that green pyramid rotates around, that blue Unit Point dot will move around the surface.  But the Unit Point alone, is really not enough to describe a rotation.  That object could point at the same unit point and spin around 360. If you picture you arm reaching out to a specific point, you still have a full degree of rotation at that position.

So to completely describe a rotation, you need to take into account the orientation of the Unit Sphere.

So the blue Unit Arrow here is full describing a rotation.  The arrow has a position on a unit sphere and 360 range it can rotate around from that point.
A rotation is that blue arrow.  When you visualize the rotation of an object, picture an arrow on a sphere surrounding it.

Now rotations have one other small point we need to take into consideration.  Rotations have a zero starting point on that sphere:

So you can think of any rotation as an animation on the surface of a sphere between two arrows: the zero arrow and your rotation.  The zero arrow is important, because it changes between different transformation spaces (for example between global and local).

If you think about rotation as a motion, it also becomes clear that rotations are very much like vectors: they can be either a delta (or difference) between rotations or description of an orientation.  The zero rotation is the reference that your rotation is against.

2) Comparison of Rotation Representations:

b)Euler Rotations
I tend to think of Eulers more as a method of generating rotations than an accurate way to describe them spatially.  The generation is done by three axis and angle rotations, one after another.  One way to imagine it as grabbing a basketball, and doing 3 pushes to attempt to align the ball with your desired rotation.    If your Euler rotation is (45, 35, -20) it really means take something at 0,0,0 add:

Euler angles are pretty useless for blending and solving rotation problems because the combinations of rotations can easily produce a situation where two of the axis are very close in alignment and begin to double up the rotation being added.  It's a very common thing for someone first learning about rotation expressions to spend alot of time pulling their hair out because no matter what they do they cannot avoid gimble lock.  Any approach they take will be intrinsically flawed from the start because the are using an rotation representation that is not up for the task they are trying to achieve.

There are a couple reasons why Euler representations are popular:
1) They can be easily represented as a set of 2D projections (and therefore fcurves) so animators like them because it provides a mechanism to easily modify and see acceleration.  Really animators are not manipulating a full description of a rotations at all, they are just masters of adding tiny sequential rotation pushes.
2) Mechanical objects that rotate on a single axis like helicopters or camera rigs can be represented well by Euler angles.  This is really because those objects are physically constructed in a manner identical to the transformation done in Euler, essentially 3 sequential axis and angle transformations.

b) Constraints and up Vectors
Another way of describing rotations in XSI is with constraints and up vectors.  This is the bread and butter of the way people are currently setting up characters.  The diagram below shows a direction constraint.

There are many constraint systems that only give us a unit point: Direction, Path with Tangency On and an IK chain.  By this description alone, there is enough information to know where the rotation is pointing, but not enough to get a Unit Arrow.

Using constraints it is another vector that is used to complete the rotation description.  This vector defined by an UpVector Constraint.  Basically what is it doing is making another Unit Point and making your unit Arrow angle towards that point.

 

So you can see there's almost a complete system for describing a rotation here.  Where it breaks apart is the fact that there is no solution when the Upvector is the same or the opposite of the direction vector.  It is also really tricky to control when the vectors are even close.  Basically this is a rotation system with two poles described by the proximity of those vectors.  Even if you use a direction constraint or IK chain in XSI and don't define an upvector, you are using one and will notice the "pole" behavior.  It is just positioned by default at y+ and can be found in the Upvector tab of any constraint.

Keep in mind though, there really is not such thing as flipping.  Rotations are always doing what you tell them, it is always the limits of your rotation system that causes unwanted behavior.

d) Axis and Angle

Another way to view a rotation is axis and angle notation:

Axis and Angle notation describes the single axis rotation that will rotate the zero arrow into a unit arrow.  It has two components: a vector that describes the axis or plane that you are rotating on, and the angle you rotate through that axis.  The rotation in the diagram above could be described as (axis.x, axis.y, axis.z, 80).  You can get and set axis and angle values in XSI through the Object Model, from both an SITransformation and an SIRotation object.

Really when you use Euler rotations you are just doing 3 axis and angle transformations where the axes are the global (or local) x,y,z vectors.

I think this way of representing rotations is very appealing to humans because it is very similar to the way we physically manipulate rotations with our hands.  When someone rotates a Ferris wheel or a basketball, the generally grab it and rotate on a single axis.

The problem is though, that this is not a very good system to blend or transform rotations.  You can see the path from the zero arrow to the unit arrow is really the long way about.   There is a much blend between the two that is rotating and spinning at the same time.

c) Unit Quaternions

I think the best way to visualize a unit quaternion (the kind used in XSI), is exactly the same as a rotation.  Take a look over Section 1 "Visualizing Rotations".  These are the exact properties of quaternions.  The main feature of quaternions is that they blend together well.  Working with quaternions to solve rotation problems is much closer to working with vectors for translations, once you get the hang of it there are not all the painful and tricky problems found in other rotation representations.

I think people can quickly get in over their head with quaternions when they try to divide it into components.  There is no need to do this to solve rotation problems in XSI.  You are provided with a full set of tools to manipulate quaternions and these can do a great deal without any need for digging deeper into the mathematical innards.  The reset of this article will cover how to study, understand and use these tools.

But if you're curious, here are a few facts about quaternions:

1) Like axis and angle, quaternions are composed of a vector and a scalar value.  The difference with quaternions is that each component of the vector is a complex (imaginary) number.
2) Complex number are written in the form a + ib, where i2 = -1.  They were first used to solve the problem  a2 + b2 = 0.
3) Don't waste your time trying to come up with an interface that manipulates (or animates) the w, x, y, z values individually.  The result is unpredictable because the components are woven together in the formula:
[cos(a/2), sin(a/2) * nx, sin(a/2)* ny, sin(a/2) * nz]
where:
a
is the angle of rotation and <nx,ny,nz> is the axis of rotation.
 

3) Using Quaternions without scripting

 a) At the core of XSI, all rotations are manipulated as quaternions.  From the UI, you can read and set but not animate quaternion values.  To read them open the Explorer and change the mode to "All Nodes(Scripting)".    From here if you look under Global or Local Orientation you can see there's a folder of "quat" channels.  If you want to read an change them, just create a custom parameter set and drag and drop them on it as proxy parameters.  I recommend dropping the local, because you get a much better slider range to test manipulating them with.

b) By default the animation mixer will mix all rotations as quaternions (this can be switched to Euler in the mixer properties tab).  If you want to do smooth quaternion blended animation just save poses of your rotations (or your entire character) and blend them together in the mixer.  Blending between expressions, constraints and keys will also be blended with quaternions.

c) Constraint blending (outside the mixer) also uses quaternions.  For example if you do two orientation constraints, and set the blend weight to .5 do a 50% slerp between the two orientations.

d) If you load the default rig or use the bi-ped proportional guide to generate a character, the spine is quaternion blended.

4) Techniques to Visualize Quaternion Operations

I've constructed a scene to make it easy to understand the relationship between quaternion inputs and outputs.
Here's the scene: quat_viewer_03

You can write a quaternion scripted operator on the Global Orientation of the Blue Object.  As inputs you can read the quaternions off of the green and yellow objects.

For an interface, you just grab the yellow and green arrow inputs and slide them around in view mode.  They will remain constrained to the surface of the sphere.  The blue arrow will show the Blue Arrow corresponding with the orientation of the blue object.

I've found this technique for studying quaternions to be very successful.  I felt like I was working blind before I created this, and since I've found it very easy and quick to solve rotation problems.  I also hook up sliders in the scripted operators so I can study how my different variables will animate.

The sphere and the arrows are subdivision surfaces, so if you're on a slow machine and want to speed things up you can reduce their subdivision.  It's also much faster to interact with the sphere if you expand this user view to full.

If you look at the scripted operator you can see to keep things simple I've kept the variable names A, B and C.  A is the green, B is the yellow and C is the blue output.

Relative Zero Arrow:

 The one thing to keep in mind when working with this scene and working with quaternions in general is that the zero arrow is arbitrary.  You can think of it as the location you eye is fixed on as you watch a rotation.  In fact, every point on the surface is being moved by a given rotation transformation, but to watch it you really need a point (in fact arrow) of reference.

In this particular scene the tip of the cones are pointed Y up.  So the zero point  is located with the vector (0,1,0) and the arrow is pointing in alignment with the x axis (1,0,0).
This is not significant for most quaternion operations (which are blends and additions of relative rotations themselves), but will be significant if you want to spatially manipulate your arrow in relation to a non-rotational reference (for example constraining rotation to a single plane).

 

5) Quaternions to/from Euler

Although quaternions can be read and manipulated through C++, scripting and scripted operators, I'm going to solely focus on scripted operators for this document.  The reason for this is that it allows you to watch rotation behavior interactively and they are very simple to create and modify.

As I said earlier, it is possible to read quaternions directly in XSI, but you need to write to write to Euler (this is for scripted ops, through scripting you can write to a full transform).  To start things off though I'm going to show how to read in an Euler of Object A, convert to Quaternion, then right back to Euler on Object B:
You can load the scene here: quat_to_euler_01



The scripted operator is located Object B > Global > Euler.

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz        Output         C.kine.rotx,roy,rotz
Ax, Ay, Az        Input            A.kine.rotx,roy,rotz

------------------MAIN  PANE-----------------------------
Select Case Out.Name
case "cx"
    ' get a rotation while converting degree inputs into rotations
    Arot.SetFromXYZAnglesValues XSIMath.DegreesToRadians(ax), XSIMath.DegreesToRadians(ay), XSIMath.DegreesToRadians(az)
    Crot.GetXYZAngles C
    'from the rotation get a quaternion
    Arot.GetQuaternion Aquat
    'at this point you are holding the quaternion Aquat and can begin to manipulate it.
    'now output the quaternion into Euler angles

    Crot.SetFromQuaternion Aquat
    Crot.GetXYZAngles C
    Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
    Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
    Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------
dim Arot, Aquat, Crot, C
set Arot = xsimath.createrotation
set Crot = xsimath.createrotation
set Aquat = xsimath.createquaternion
set C = xsimath.createvector3
-------------------------------------------------------------

So before I go further I'll make a few points about making scripted operators, and this operator in particular:
1) It's faster to keep your variables that are objects like Rotations and Quaternions as global operators.  This means define them in the bottom pane.  Making the objects over and over again in a loop will degrade performance.
2) Most of the object  manipulation I'm doing is with the Object Model.  The best way to get used to using these is to get used to the help for these objects.  Click on the ? in the scripting window and look under: Reference > Reference Entities > Objects > SIQuaternion,  SIRotatation, SITransformation, SIVector, SIMatrix4 and XSIMath.
3) Although rotations are described in the interface of XSI in degrees, the are described internally and in the Object model in radians.  That's why you need to use the conversion routines xsimath.DegreesToRadians and xsimath.radianstodegrees at the beginning and end of this function.
4) Rotation calculations need to be done in a single block for all three Euler angles.  That's why this operator calculates all three rotation values in the evaluation of cx and then carries them over to be written on cy an cz.
5) When connecting your inputs and outputs to a scripted operator, just drag and drop them from the explorer.

6) Reading quaternions

So in fact the last operator we wrote can be a bit cleaner, you can directly read the quaternion values instead of picking up the Euler values.  Here's the revised version of the scene: read_quat_01

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz               Output         C.kine.rotx,roy,rotz
Aw, Ax, Ay, Az        Input            A.kine.quatw, quatx, quaty, quatz

------------------MAIN  PANE-----------------------------
Select Case Out.Name
case "cx"
    Aquat.set aw, ax, ay, az
    Crot.SetFromQuaternion Aquat
    Crot.GetXYZAngles C
    Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
    Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
    Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------
dim Aquat, Crot, C
set Crot = xsimath.createrotation
set Aquat = xsimath.createquaternion
set C = xsimath.createvector3
-------------------------------------------------------------

You can see we saved quite a bit of hassle at the beginning reading the quat's directly.  This will be the base we use to manipulate quaternions.

7) Adding 2 Quaternions

So now lets begin to tackle quaternion problems.  The first is if you want to add to rotations together.  A typical example of this is if you want an object that was a "double child" of two objects and inherited rotations from both of them.
Open the scene: add_quats_01

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz               Output         C.kine.rotx,roy,rotz
Aw, Ax, Ay, Az        Input            A.kine.quatw, quatx, quaty, quatz
Bw, Bx, By, Bz         Input            B.kine.quatw, quatx, quaty, quatz

------------------MAIN  PANE-----------------------------
Select Case Out.Name
case "cx"
Aquat.set aw, ax, ay, az
Bquat.set bw, bx, by, bz
Cquat.Mul Aquat, Bquat
Crot.SetFromQuaternion Cquat
Crot.GetXYZAngles C
Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------
dim Aquat, Bquat, Cquat, Crot, C
set Aquat = xsimath.createquaternion
set Bquat = xsimath.createquaternion
set Cquat = xsimath.createquaternion
set Crot = xsimath.createrotation
set C = xsimath.createvector3

-------------------------------------------------------------

One last thing to keep in your mind when adding rotations (multiplying quaternions) is that it is not the same to add A + B as it is to add B + A.  This is know as rotations being non-commutative.

If you think of a rotation as a kind of path with a spin on it, add when you add you do so by matching the orientation of your last spin, this does make sense.

8) Blending 2 Quaternions

One of the greatest advantages of using quaternions is they are easy and efficient to blend using a slerp function.  To make C a 50% blend between A and B, just:
C.slerp A, B, .5

The best way to setup a slerp that slides interactively from 0 to 1 is to setup a slider as input for your scripted operator.  To do that:
a) go to the connections pull-down in your scripted op.
b) Hit "New" to create a new slider.  For this example lets call it "mySlider".
c)  read it in your scripted op with the line
myVariable  = In_UpdateContext.mySlider.Value

One tip I've figured out using sliders: you will get better performance if you use a slider outside the operator, but as of 2.0 it is difficult to hook up such sliders through presets.

 So here's an example Scene: slerp_01

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz               Output         C.kine.rotx,roy,rotz
Aw, Ax, Ay, Az        Input            A.kine.quatw, quatx, quaty, quatz
Bw, Bx, By, Bz         Input            B.kine.quatw, quatx, quaty, quatz
---------------------SLIDERS-------------------------------
mySlerp     Double     range 0 to 1            default .5
------------------MAIN  PANE-----------------------------
Select Case Out.Name
case "cx"
    mySlerp = In_UpdateContext.mySlerp.Value
    Aquat.set aw, ax, ay, az
    Bquat.set bw, bx, by, bz
    Cquat.Slerp Aquat, Bquat, mySlerp
    Crot.SetFromQuaternion Cquat
    Crot.GetXYZAngles C
    Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
    Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
    Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------
dim Aquat, Bquat, Cquat, Crot, C', mySlerp
set Aquat = xsimath.createquaternion
set Bquat = xsimath.createquaternion
set Cquat = xsimath.createquaternion
set Crot = xsimath.createrotation
set C = xsimath.createvector3
-------------------------------------------------------------

9) Reversing Rotations

Many rotation operations you do will require you to get a certain rotation, but backwards.
To get this all you need to do is Invert the Quaternion.
Here's an example scene:  Invert_01

There are two methods in the OM for quaternions to do this:
SIQuaternion.Invert SourceQuat
or
SIQuataernion.InvertInPlace

10) Subtracting Quaternions.

So next we will look at subtracting one rotation from another.  There are many applications for this, a couple are:
a) To find the offset or delta between to rotations so the same transformation can be applied to other objects.
b) To figure out angular velocity between updates.
c) To divide rotations into parts so the components can be manipulated differently.
and many others....

So to get the reverse of a rotation, we just have to keep in mind where we are coming from and where we are going to.
So to get from A to B we need to Inverse(A) * B

If we wanted this new rotation C, to be described by the Object Model:

A.InvertinPlace
C.mul A, B

and because we are multiplying quaternions the opertation is non-commutative.

Here's a scene showing the behavior: sub_quats_01

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz               Output         C.kine.rotx,roy,rotz
Aw, Ax, Ay, Az        Input            A.kine.quatw, quatx, quaty, quatz
Bw, Bx, By, Bz         Input            B.kine.quatw, quatx, quaty, quatz

------------------MAIN  PANE----------------------------
Select Case Out.Name
case "cx"
Aquat.set aw, ax, ay, az
Bquat.set bw, bx, by, bz
Aquat.InvertInPlace
Cquat.Mul Aquat, Bquat

Crot.SetFromQuaternion Cquat
Crot.GetXYZAngles C
Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------
dim Aquat, Bquat, Cquat, Crot, C
set Aquat = xsimath.createquaternion
set Bquat = xsimath.createquaternion
set Cquat = xsimath.createquaternion
set Crot = xsimath.createrotation
set C = xsimath.createvector3

-------------------------------------------------------------

11) Scaling Rotations
So now that we have a library of methods to generate rotation behaviors we can start to pull them together into larger tools.
One such challenge is to take a given rotation and scale it 3 1/2 times.  It's easy enough to scale rotations between 0 and 1 times their original values (just slerp them with an empty or zero quat), but it's harder to scale over 1.

So if adding rotations is done by multiplying quaternions, the solution I use is a loop of quaternion multiplying.  If I want to scale my current rotation by 3.5, I multiply the quaternion by itself 3 times and 4 times and do a slerp of .5 of the two results.

 Here's a scene that does this multiplying loop:  scaling_quats

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz               Output         C.kine.rotx,roy,rotz
Aw, Ax, Ay, Az        Input            A.kine.quatw, quatx, quaty, quatz
---------------------SLIDERS-------------------------------
slider     Double     range 0 to 5            default
------------------MAIN  PANE-----------------------------
Select Case Out.Name
case "cx"
    slider = In_UpdateContext.slider.Value
    Aquat.set aw, ax, ay, az
    scale_quat Aquat, slider, Cquat
    Crot.SetFromQuaternion Cquat
    Crot.GetXYZAngles C
    Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
    Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
    Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------

dim Aquat, Cquat, Crot, C
set Aquat = xsimath.createquaternion
set Cquat = xsimath.createquaternion
set Crot = xsimath.createrotation
set C = xsimath.createvector3

set highQuat = xsimath.createquaternion
set MulQuat = xsimath.createquaternion

'------------------------------------------
' Scale Quat
'------------------------------------------
' to multiply our rotation by 4.3 we need to get the
' lowerInt(4), the upperInt(5) and slerp between them the
' remainder (.3).
'..........................................
function scale_quat(in_d, in_slider, MulQuat)

    dim i, remainder
    set lowQuat = xsimath.createquaternion

    if in_slider >= 1 then
    'find the Quat from the lower Int(lowQuat)
    for i = 0 to int(in_slider) - 1
        lowQuat.MulInPlace in_d
    next
    end if

'find the Quat from the upper Int(highQuat)
    highQuat.Mul lowQuat, in_d

'now slerp the remainder
remainder = in_slider - Int(in_slider)
MulQuat.slerp lowQuat, highQuat, remainder

end function
'------------------------------------------
-------------------------------------------------------------
12) Constraining Rotations to a plane

So here's the final rotation manipulation for this article: constraining rotations to a plane.  It's not really a quaternion manipulation we are doing, but it comes up enough I figured it should be covered.

To perform this projection, we have to go back a bit and take into account that our placement of the Unit Point (and Unit Arrow) are arbitrary.  But it is this arbitrary vector that we'll use to define the our axis of projection, and to define the plane we are constraining to (ie it is the normal of the plane to constrain to).  Here's an outline of the process I use:

1) Get the Vector to the Zero Point, in all my example scenes it's y up so 0,1,0.
2) Transform that vector with your current rotation to get the Unit Point (this is your arrow, but without any spin).
3) Find the angle between the two vectors.  The "equator" around your zero vector will have an angle of 90. So you can find the angle remaining to hit that plane by subtracting it from 90.
4) Find the axis for your extra rotation which is the normal of the plane defined by your two vectors.  Get this from a cross-product.
5) Build your new rotation to reach the equator.
6) Add on the new rotation by multiplying it with your current.

Of course there's no good answer for this when your Zero Point equals you Unit Point.

Here's a scene that shows this behavior: Constrain_to_plane_03

-------------INPUTS AND OUTPUTS---------------------
Cx, Cy, Cz               Output         C.kine.rotx,roy,rotz
Aw, Ax, Ay, Az        Input            A.kine.quatw, quatx, quaty, quatz

------------------MAIN  PANE----------------------------
Select Case Out.Name
case "cx"

    slider = In_UpdateContext.slider.Value
    Aquat.set aw, ax, ay, az

    zeroPoint.set 0,1,0

    'transform zeroPoint to become the unit point
    Trans.SetTranslation zeroPoint
    Atrans.SetRotationFromQuaternion Aquat
    Trans.MulInPlace Atrans
    Trans.GetTranslation unitPoint

    ' get the cross product (vector perpendicular to the plane defined by
    ' two vectors) and the angle between the unit and zero vectors

    angle = zeroPoint.angle(unitPoint)
    cp.cross zeroPoint, unitPoint

    'now we can make a vector to the unit point with no extra spin

    'so to get to the xz plane we need an angle of 90, so get the missing amount
    angle = xsimath.degreestoRadians(90) - angle

    extraRot.SetFromAxisAngle cp, angle

    Arot.SetFromQuaternion Aquat
    Crot.Mul Arot, extraRot

    Crot.GetXYZAngles C
    Out.Value = xsimath.radianstodegrees(C.x)
case "cy"
    Out.Value = xsimath.radianstodegrees(C.y)
case "cz"
    Out.Value = xsimath.radianstodegrees(C.z)
end select
-----------------BOTTOM PANE--------------------------
dim Aquat, Cquat, Crot, C, zeroPoint, unitPoint
dim angle, cp, extraRot

set zeroPoint = xsimath.createvector3
set unitPoint = xsimath.createvector3
set cp = xsimath.createvector3
set C = xsimath.createvector3

set trans = xsimath.createtransform
set atrans = xsimath.createtransform

set extraRot = xsimath.createrotation

set Aquat = xsimath.createquaternion
set Cquat = xsimath.createquaternion
set Crot = xsimath.createrotation
set Arot = xsimath.createrotation

-------------------------------------------------------------

So if you look closely at this it should become clear you can use this technique to project onto any arbitrary plane you want.
 

Credits: And finally special thanks to Brent McPherson, Mathieu Mazerolle,  and Marie-Claude Frasson for teaching me about quaternions, and being very patient while I asked millions of questions :)